diff --git a/LegacyCelerisLab/kernels/macros.h b/LegacyCelerisLab/kernels/macros.h index d3af419..7a2466d 100644 --- a/LegacyCelerisLab/kernels/macros.h +++ b/LegacyCelerisLab/kernels/macros.h @@ -33,5 +33,5 @@ #define V_TAYLOR 0b00000001 // variables -#define N_OBJS 6 +#define N_OBJS 7 // #define N_SENS 2 \ No newline at end of file diff --git a/configs/config_lbm_karman_2000x600.json b/configs/config_lbm_karman_2000x600.json new file mode 100644 index 0000000..cb3007c --- /dev/null +++ b/configs/config_lbm_karman_2000x600.json @@ -0,0 +1,50 @@ +{ + "_doc": "Karman Cloak Re100: uniform inlet, free-slip walls, 2000x600 grid. Pinball centered.", + "grid": { + "lattice_model": "D2Q9", + "nx": 2000, + "ny": 600, + "nz": 1 + }, + "physics": { + "data_type": "FP32", + "viscosity": 0.004, + "velocity": 0.01, + "rho": 1.0 + }, + "method": { + "collision": "MRT", + "streaming": "double_buffer", + "store_precision": "FP32", + "ddf_shifting": false, + "les": { + "enabled": false, + "cs": 0.16, + "closed_form": true + }, + "trt": { + "magic_param": 0.1875 + }, + "inlet": { + "profile": "uniform", + "scheme": "regularized", + "trt_neq_damp": 0.5, + "regularized_neq_damp": 0.5 + }, + "outlet": { + "mode": "neq_extrap", + "backflow_clamp": true, + "blend_alpha": 0.7, + "srt_neq_damp": 0.5 + }, + "y_wall_bc": "free_slip", + "omega_guard": { + "min": 0.01, + "max": 1.99 + } + }, + "cuda": { + "threads_per_block": 256, + "compute_capability": "auto" + } +} diff --git a/src/CCD_analysis/Lyu23.md b/src/CCD_analysis/Lyu23.md new file mode 100644 index 0000000..9c96b00 --- /dev/null +++ b/src/CCD_analysis/Lyu23.md @@ -0,0 +1,1000 @@ +# Canonical correlation decomposition of numerical and experimental data for observable diagnosis + +B. Lyu $^{1}$ † + +$^{1}$ State Key Laboratory for Turbulence and Complex Systems, College of Engineering, Peking University, Beijing 100871, China + +(Received xx; revised xx; accepted xx) + +A flow decomposition method based on canonical correlation analysis is proposed in this paper to optimally dissect complex flows into mutually orthogonal modes that are ranked by their cross-correlation with an observable. It is particularly suitable for identifying the observable-correlated flow structures while effectively excluding those uncorrelated, even though they may be highly energetic. Therefore, this method is capable of extracting coherent flow features under low signal-to-noise ratios. A numerical validation is conducted and shows that the method can robustly identify the observable-correlated flow events even though the underlying signal is corrupted by random noise that is four orders of magnitude stronger. The temporal sampling frequency and duration of the observable determine the maximum and minimum frequencies to be resolved in the cross-correlation respectively, while those of the flow are to ensure convergence. These criteria are validated using synthetic examples. The decomposition method is subsequently used to analyse a turbulent channel flow, a subsonic turbulent jet and an unsteady vortex shedding from a cylinder, showing the effectiveness of observable-correlated structure identification and order reduction. This decomposition represents a data-driven method of effective order reduction for highly noisy numerical and experimental data and is suitable for identifying the source and descendant events of a given observable. It is hoped that this method will join the existing flow diagnosis tools, in particular for observable-related diagnosis and control. + +# 1. Introduction + +Many natural flows exhibit complex behaviour, such as the boundary layer formed over a sand dune or compressed air inside an aeroengine. This is particularly true at high Reynolds numbers, where most realistic engineering flows occur, because turbulence comes into play exhibiting a wide range of temporal and spatial scales. To understand, model, and possibly exert control on these flows, it is crucial to extract dominant structures and reduce the systems' degrees of freedom. + +Extensive research has been conducted to extract coherent features and decompose complex flows into a collection of simple modes. Well-established methods include the Proper Orthogonal Decomposition (POD), Dynamic Mode Decomposition (DMD), resolvent/input-output analysis and global stability analysis (Taira et al. 2017). Among these, POD and DMD fall into the category of data-driven approaches, while the resolvent/input-output analysis and global stability analysis are model-based. + +POD (Lumley 1967; Berkooz et al. 1993) is a particularly well-known data-driven method and represents a powerful tool for feature extraction and order reduction. Originating from Principal Component Analysis (PCA) in classical statistics, POD decomposes a complex flow into mutually orthogonal modes ranked by their fluctuation energy. If a flow is comprised of a few energetic coherent structures, POD effectively identifies them as leading-order modes. A linear combination of these leading-order modes then forms an optimal reduced-order representation of the total flow. POD may be used to extract the spatial or temporal structures (Lumley 1970; Sirovich 1987). These two structures are coupled, with the temporal structures representing the temporal variation of their corresponding spatial modes, and the spatial modes representing the spatial distribution of their corresponding temporal modes (Aubry 1991). This leads to the so-called Bi-orthogonal Decomposition (BOD). Recent years have also seen the increasingly widely-used Spectral Proper Orthogonal Decomposition (SPOD) in studying turbulent flows (Towne et al. 2018). In addition, to better capture the structures in transient and intermittent flows, conditional space-time POD (Schmidt and Schmid 2019) and multidimensional empirical mode decompositions (Souza et al. 2024) are proposed. These techniques are used to examine the acoustic bursts, the onset and evolution of the dynamic stall and intermittent vortex pairs, showing advantageous capability in resolving transient and intermittent events. It is worth noting that since POD relies on the underlying coherence within the flow to work, it is capable of identifying the flow structures that are dynamically nonlinear compared to linear model-based approaches. + +While POD aims to identify the coherent structures within a complex flow, DMD aims to extract temporal evolutionary information of the underlying dynamics captured in the data (Schmid 2010). The resulting representation is a dynamical system of fewer degrees of freedom. DMD starts by assuming a linear mapping between a sequence of the flow data, and the dynamics is extracted by examining the eigenvalues of a similarity matrix. For a linear system, this amounts to identifying the eigenmodes of the system. For nonlinear systems, DMD is connected with the modes of the so-called Koopman operator (Koopman 1931; Mezić 2013; Schmid 2022). Unlike POD, DMD modes capture the main “contributions” to the overall dynamics embedded in the data sequence. Recent years have seen numerous variants of DMD such as the extended DMD (Williams et al. 2015) and Residual DMD (Colbrook et al. 2023). More details on the recent development of DMD can be found in the recent review by Schmid (2022). + +As mentioned above, both POD and DMD are data-driven, while the resolvent analysis is based on the modal analysis of a linear operator. The resolvent analysis has an early origin in control theory and is based on the pseudospectrum of an operator (Trefethen et al. 1993; Taira et al. 2017), rather than the spectrum. For example, when the flow is decomposed into a base part and a fluctuation part, the Navier-Stokes equations can be rewritten and interpreted as a forced linear system, by which the evolution of the fluctuation part is governed. The nonlinear terms are collected on the right-hand side and interpreted as the forcing of the system. The resolvent modes are ranked by the energy gain between the response and forcing. Therefore, the resolvent analysis examines the gain properties of the linearized operator and has been successfully used to study turbulence from a linearized Navier-Stokes equation point of view (Farrell and Ioannou 1993; Mckeon 2010). Recent studies also show that the leading-order resolvent modes match the leading-order SPOD modes extracted from a numerically simulated high-speed jet (Schmidt et al. 2018). The input-output analysis (Jovanović 2021) is similar to the resolvent analysis in that a modal analysis is performed on a linearized operator. Input-output analysis differs from the conventional resolvent analysis in that a weight may be added to the operator to bias both the forcing and response towards interested domains or observables (Jeun et al. 2016). Therefore, input-output analysis may be regarded as a weighted resolvent analysis. + +In contrast to the resolvent analysis, model-driven global stability analysis (Theofilis 2011) examines the eigenvalue properties of an operator linearized around a base flow with multiple inhomogeneous spatial directions. In particular, it pays special attention to unstable modes, which would dominate the linear response of the system at large times. Note that through global stability analysis, the stable modes can also be obtained, which may play an important role in determining the transient dynamics of underlying flows. This is particularly true in fluid mechanics, where the linearized operators are often non-normal (Trefethen et al. 1993) and the transient growth can become crucial in determining the flow stability. In addition, an adjoint analysis of the operator may be performed to examine the receptivity problem, yielding modes that are similar to the optimal forcing modes in the resolvent analysis. + +POD and DMD, together with their variants, are common data-driven flow decomposition methods used in fluid mechanics. These provide important tools for probing the structures and dynamics of an underlying dynamical system. The ultimate goal of identifying the dominant structures or dynamics is, however, often to understand and possibly control some observables of the flow, such as to reduce the drag of a cylinder, minimise the unsteady force of a wing, or abate the noise emission from a jet. However, because POD modes are ranked by their fluctuation energy, the leading-order modes are not necessarily the most important structures as far as the observable is concerned, although they do carry the largest energy. For example, a large coherent structure effectively extracted from a turbulent subsonic jet using POD may be very inefficient at generating noise. In other words, the leading-order POD mode may not be the leading-order noise-generating flow structure. For example, it has been shown that a substantial number of near-field POD modes are required to reconstruct the acoustic field (Freund and Colonius 2009). Similarly, DMD extracts the dominant dynamics embedded within the flow without taking their connection with any observable into account. Consequently, the leading-order dynamic mode does not necessarily represent the flow events connected with the leading-order dynamics of the observables. + +That the energy rank may not be an appropriate measure, in particular for an observable-related diagnosis, is a well-recognised limitation of POD (Rowley 2005; Schmid 2010). One widely-used approach to overcome this difficulty is to use different norms to bias the decomposition towards interested observables or to use the extended POD (Maurel et al. 2001; Borée 2003). For example, Freund and Colonius (2009) performed the POD decomposition of a turbulent jet using various norms, including the near-field turbulent kinetic energy, near-field pressure, and far-field pressure. When the far-field pressure norm is used, the near-field flow quantities drop out in the correlation matrix and the resulting modes are effectively ranked only by the far-field pressure. Although the near-field flow can still be projected onto the far-field basis, the resulting near-field mode does not necessarily form a direct continuation of the far-field physics, particularly when the near- and far-field exhibit completely different dynamics or the far-field and near-field variables are characterised by pronounced phase delays. Note that the balanced POD proposed by Rowley (2005) is another similar technique to overcome the energy norm limitation of POD, which may be viewed as a special form of POD when the observability Gramian is used as the norm. + +On the other hand, the resolvent and input-output analyses decompose the flow to maximise the energy gain between the output and forcing based on the spectral theory of linear operators. Hence, the observable may be directly included in the choice of output. The resolvent and input-output analysis represent powerful tools to diagnose the flow structure and are capable of providing insightful understanding into a variety of turbulent flows (Mckeon 2010; Sharma and Mckeon 2013). In order to do so, a linearized operator describing the underlying system is often needed. In some cases, however, such an operator may not be readily known, while in others the linearized operator may not be an appropriate representation of the dynamical system, particularly in highly nonlinear systems. For example, an input-output analysis was performed on compressible subsonic and supersonic jets and found that a considerable number of modes were required to reconstruct the acoustic energy of subsonic jets (Jeun et al. 2016), which may be partly due to the limitation imposed by linearity. Such a limitation is also applicable to global stability analysis, where a linearized operator must be known in advance. + +In this paper, we aim to develop a data-driven flow decomposition method that is suitable for observable diagnosis based on flow and observable snapshots instead of linear operators. Instead of redefining the POD energy norm to bias towards the observable, the decomposition aims to introduce a rank based on a cross-correlation norm between the resulting modes and the observable, hence including both the flow and observable data in the correlation matrix. The decomposition method falls under the framework of canonical correlation analysis (CCA) (Hotelling 1936) in classical statistics. This paper is structured as follows: section 2 shows a mathematical formulation of the decomposition method. The physical significance of the resulting modes, the frequency and wavenumber resolutions, the effect of including multiple observables and the connection of the present decomposition to POD and the extended POD are discussed in detail sequentially. Section 3 validates the method by performing the decomposition on multiple synthetic flow fields. The effects of varying sampling frequency, duration and including multiple observables are also thoroughly validated. Section 4 applies this technique to both numerical and experimental data, demonstrating the potential use of such a method. The following section concludes the paper and lists some future work. + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/75159b30fee58ee8c37eab1182a7d5ababa12d31b362597a80ac0b808f8a8b31.jpg) + + + +Figure 1: Schematic illustration of the two-dimensional flow snapshots sampled at time $t_{i}, i = 1, 2, 3, \ldots, N$ . Each snapshot contains flow data in both x and y directions, where x and y denotes the Cartesian coordinates of the flow domain. + + +# 2. The canonical correlation decomposition + +# 2.1. The decomposition procedure + +Assume that we have a sequence of snapshots $u_{i}$ obtained by sampling a flow field $u(\boldsymbol{x}, t)$ at time $t = t_{i}$ , where x represents the coordinates of the flow domain and i is an integer that takes the value of $1, 2, 3, \ldots, N$ . If the snapshots $u_{i}$ are sampled in time, $t_{i}$ increases sequentially as i increases, as shown in figure 1. If $u_{i}$ are, however, sampled in the ensemble space, each $t_{i}$ refers to the sampling time in its corresponding independent realisation and can therefore be independent of each other. In the most general case, $u_{i}$ can be sampled both in the time and ensemble space. Each snapshot of this sequence is obtained by discretizing the spatial domain on a mesh and represented by a column vector of length M. We write this snapshot sequence compactly in a matrix notation as + +$$ +\boldsymbol {U} = \left[ \boldsymbol {u} _ {1}, \boldsymbol {u} _ {2}, \boldsymbol {u} _ {3}, \dots , \boldsymbol {u} _ {N} \right]. \tag {2.1} +$$ + +For each snapshot $u_{i}$ , which is obtained by sampling a flow field at $t_{i}$ , assume that we can simultaneously sample an interested observable $p(t)$ of this flow at time $t_{i} + \tau_{j}$ , where j is an integer and takes the values of 1, 2, 3, ...Q with Q being a positive integer. We therefore obtain a sequence of the sampled observable $p_{i,j}$ , $j = 1, 2, 3...Q$ . Note that the sequence $p_{i,j}$ can be sampled at an earlier or later time of $t_{i}$ , depending on whether $\tau_{1}$ is a negative or positive value, respectively. This is important, and we will discuss its significance in the rest of this paper. + +For each integer i, we can define a column vector $p_{i}$ such that + +$$ +\boldsymbol {p} _ {i} = [ p _ {i, 1}, p _ {i, 2}, p _ {i, 3}, \dots p _ {i, Q} ] ^ {T}, \tag {2.2} +$$ + +where T denotes transpose. We then form a matrix P such that + +$$ +\boldsymbol {P} = [ \boldsymbol {p} _ {1}, \boldsymbol {p} _ {2}, \dots , \boldsymbol {p} _ {N - 1}, \boldsymbol {p} _ {N} ]. \tag {2.3} +$$ + +The key step is to construct a matrix A, representing the cross-correlation matrix between the flow and the observable, such that + +$$ +\boldsymbol {A} = \frac {1}{N \sqrt {Q}} \boldsymbol {P} \boldsymbol {U} ^ {\dagger}, \tag {2.4} +$$ + +where $\dagger$ denotes the Hermitian adjoint. The Hermitian adjoint here allows both P and U to be complex matrices. This is useful because both the observable and flow field can be just a Fourier component of the total fields (see the end of section 2.1 for more details). In the case where only real matrices are involved, the Hermitian adjoint $\dagger$ reduces to the simple transpose T. + +We then perform the standard Singular Value Decomposition (SVD) of matrix A, such that + +$$ +\boldsymbol {A} = \boldsymbol {R} \boldsymbol {\Sigma} \boldsymbol {V} ^ {\dagger}, \tag {2.5} +$$ + +where R and V are $Q \times Q$ and $M \times M$ unitary matrices respectively, while $\Sigma$ is a diagonal matrix of $Q \times M$ with the singular values $\sigma_{j}$ ( $j = 1, 2, 3 \ldots, \min(M, Q)$ ) as its diagonal elements. The column vectors of V represent the desired modes of the flow field $u_{i}$ , while those of R represent the normalised cross-correlation functions between the resulting modes and the observable. From SVD, it can be readily shown that these modes are mutually orthonormal and form a complete basis of $R^{M}$ . Therefore, the flow field $u_{i}$ can be conveniently decomposed as + +$$ +\boldsymbol {u} _ {i} = \sum_ {k = 1} ^ {N} a _ {k} (t _ {i}) \boldsymbol {v} _ {k}, \tag {2.6} +$$ + +where $v_{k}$ denotes the k-th column of V while $a_{k}(t_{i})$ denotes its corresponding expansion coefficient at time $t_{i}$ , or equivalently, + +$$ +u (\boldsymbol {x}, t) = \sum_ {k = 1} ^ {N} a _ {k} (t) \phi_ {k} (\boldsymbol {x}), \tag {2.7} +$$ + +where $\phi_{k}(\boldsymbol{x})$ denotes the basis function corresponding to $v_{k}$ , while $a_{k}(t)$ is the expansion coefficient of $u(\boldsymbol{x},t)$ using the basis $\phi_{k}(\boldsymbol{x})$ . As shown in section 2.2, these modes are ranked by their cross-correlation with the observable, and a significant order reduction may be expected if only a small number of modes are pronouncedly correlated with the observable. As will be shown in section 2.2, the decomposition method falls under the framework of CCA, therefore it will be referred to as the canonical correlation decomposition (CCD) in the rest of this paper. + +Note that, as mentioned above, both the observable and the flow can be just a Fourier component of the total fields. For example, the observable may be $\tilde{p}_{\omega}$ while the flow may be $\tilde{u}_{\omega}$ , where $\tilde{p}_{\omega}$ and $\tilde{u}_{\omega}$ represent the temporal Fourier components of the observable and flow at angular the frequency $\omega$ , respectively. In practice, a long flow snapshot sequence $u(\boldsymbol{x}, t_{k})$ ( $k = 1, 2, 3 \ldots$ ) obtained in experiments or simulations may be first partitioned into N segments; each segment may be regarded as a realisation in the ensemble space and then Fourier transformed in time and/or space to form the $u_{i}$ ( $i = 1, 2 \ldots N$ ) shown in (2.1). Similarly, a long observable sequence $p(t_{k} + \tau_{1})$ obtained in experiments or simulations may be partitioned into N segments; the ith segment is then Fourier transformed with respect to $t_{k}$ ( $\tau_{1}$ is a constant) to obtain $\tilde{p}_{\omega i}(\tau_{1})$ . In a similar manner, $\tilde{p}_{\omega i}(\tau_{2})$ , $\tilde{p}_{\omega i}(\tau_{3}), \ldots$ $\tilde{p}_{\omega i}(\tau_{Q})$ can be obtained, which are just $p_{i,2}$ , $p_{i,3}, \ldots p_{i,Q}$ shown in (2.2) ( $\tilde{p}_{\omega i}(\tau_{1})$ constitutes $p_{i,1}$ ). Note that when the observable and the flow are sampled at different frequencies, proper temporal alignment of them for each realisation must be ensured according to those described in section 2.1. Care must also be taken regarding the frequency resolutions of the flow and the observable when the Fourier transform is performed. CCD can then be performed according to (2.3) to (2.5), which may be regarded as a form of CCD decomposition in the spectral space. + +# 2.2. Physical significance of CCD modes + +The CCD represents an optimal decomposition that maximises the cross-correlation between the flow field u and the observable p. This can be shown mathematically as follows. Assuming the flow field is described by the function $u(\boldsymbol{x}, t)$ while the observable by $p(t + \tau)$ , where $\tau$ represents the time delay between flow and the observable. We form the cross-correlation $R(\tau, \boldsymbol{x})$ using + +$$ +R (\tau , \boldsymbol {x}) = \left\langle p ^ {*} (t + \tau) u (\boldsymbol {x}, t) \right\rangle , \tag {2.8} +$$ + +where $*$ represent the complex conjugate, while $\langle\cdot\rangle$ represents the temporal or ensemble average. In the latter case, the statistical processes represented by u and p are assumed to be stationary. For non-stationary processes, (2.8) explicitly depends on t, but the following derivation can still proceed. + +First, let us define an inner product in the Hilbert space defined on a domain $\Omega$ such that + +$$ +(f, g) = \int_ {\Omega} f (\boldsymbol {x}) g ^ {*} (\boldsymbol {x}) \mathrm{d} x ^ {n}, \tag {2.9} +$$ + +where $f(\boldsymbol{x})$ and $g(\boldsymbol{x})$ denote two functions within this space and n represents the dimension of $\Omega$ . A norm is therefore defined as $||f|| = (f, f)^{1/2}$ . Suppose we wish to find a function $\phi(\boldsymbol{x})$ of unit norm, such that the inner product between $R(\tau, \boldsymbol{x})$ and $\phi(\boldsymbol{x})$ , i.e. $(R, \phi)$ , obtains its maximum value in the $L_{2}$ norm. Mathematically, this is equivalent to + +$$ +\max _ {| | \phi | | = 1} \frac {1}{T} \int_ {\tau_ {0}} ^ {\tau_ {0} + T} | (R, \phi) | ^ {2} \mathrm{d} \tau , \tag {2.10} +$$ + +where $|\cdot|$ represents the complex modulus, and $\tau_{0}$ and T are two constants chosen such that the integration includes the entire interval where the integrand obtains non-negligible values. + +Physically, this amounts to finding the optimal function $\phi$ that most correlates with the observable. This is because the ensemble average in (2.8) commutes with the inner product in (2.10), i.e. + +$$ +(R, \phi) = \langle p ^ {*} (t + \tau) a _ {\phi} (t) \rangle , \tag {2.11} +$$ + +where $a_{\phi}(t)$ represents the expansion coefficient of the flow field u using the basis $\phi$ , i.e. + +$$ +a _ {\phi} (t) = (u, \phi). \tag {2.12} +$$ + +Clearly, we see from (2.11) and (2.12) that $(R,\phi)$ represents the cross-correlation function between the mode $\phi$ and the observable p. The $L_{2}$ norm of $(R,\phi)$ defined over an interval of length T is a natural measure of the correlation level between $\phi$ and p. We therefore define the correlation strength $C_{e}$ as the average of $|(R,\phi)|^{2}$ over the interval $[\tau_{0},\tau_{0}+T]$ , i.e. + +$$ +C _ {e} = \frac {1}{T} \int_ {\tau_ {0}} ^ {\tau_ {0} + T} | (R, \phi) | ^ {2} \mathrm{d} \tau . \tag {2.13} +$$ + +Evidently, if $\phi(\boldsymbol{x})$ maximises $C_{e}$ , it represents a flow structure that most correlates with the observable p. + +The function $\phi(\boldsymbol{x})$ that we seek can be obtained from an eigenvalue problem as follows. We know that $\phi(\boldsymbol{x})$ is a function of unit norm that yields a maximum $C_{e}$ , i.e. $\phi(x)$ satisfies + +$$ +\max _ {| | \phi | | = 1} \frac {1}{T} \int_ {\tau_ {0}} ^ {\tau_ {0} + T} | (R, \phi) | ^ {2} \mathrm{d} \tau . \tag {2.14} +$$ + +Classic calculus of variation shows that a necessary condition for (2.14) to hold is that $\phi$ is an eigenfunction of the correlation tensor, i.e. + +$$ +\int_ {\Omega} B (\boldsymbol {x}, \boldsymbol {x} ^ {\prime}) \phi (\boldsymbol {x} ^ {\prime}) \mathrm{d} x ^ {\prime n} = \lambda \phi (\boldsymbol {x}), \tag {2.15} +$$ + +where the correlation tensor is defined by + +$$ +B (\boldsymbol {x}, \boldsymbol {x} ^ {\prime}) = \frac {1}{T} \int_ {\tau_ {0}} ^ {\tau_ {0} + T} R (\tau , \boldsymbol {x}) R ^ {*} (\tau , \boldsymbol {x} ^ {\prime}) \mathrm{d} \tau , \tag {2.16} +$$ + +and the eigenvalue $\lambda$ corresponds to $C_e$ defined in (2.13) (Riesz and Nagy 1955). Clearly, the maximum $C_e$ is given by the largest eigenvalue. + +When the flow field and the observable are discretized, we can show that after multiplied by $\sqrt{Q}$ the matrix A defined in section 2.1 is identical to a discretized form of $R^{*}(\tau, \boldsymbol{x}')$ . The correlation tensor $B(\boldsymbol{x}, \boldsymbol{x}')$ then reduces to $A^{\dagger}A$ because + +$$ +B (\pmb {x}, \pmb {x} ^ {\prime}) = \frac {1}{T} \int_ {\tau_ {0}} ^ {\tau_ {0} + T} R (\tau , \pmb {x}) R ^ {*} (\tau , \pmb {x} ^ {\prime}) \mathrm{d} \tau \approx \frac {1}{Q} \sum_ {i = 1} ^ {Q} R (\tau_ {i}, \pmb {x}) R ^ {*} (\tau_ {i}, \pmb {x} ^ {\prime}) = \pmb {A} ^ {\dagger} \pmb {A}, (2. 1 7) +$$ + +where $\tau_{i}$ is the discretized values of $\tau$ . Equation 2.15 therefore reduces to a discretized eigenvalue problem of the matrix $A^{\dagger}A$ , i.e. + +$$ +\boldsymbol {A} ^ {\dagger} \boldsymbol {A} \boldsymbol {v} _ {k} = \lambda_ {k} \boldsymbol {v} _ {k}, \tag {2.18} +$$ + +where $v_{k}$ as defined in section 2.1 is the discretized form of the k-th eigenfunction $\phi(\boldsymbol{x})$ , while $\lambda_{k}$ is the k-th $\lambda$ in (2.15) subject to a discretization constant, whose exact value often carries no significance in practice. The eigenvalue problem of (2.18) is equivalent to the singular value decomposition shown in (2.5). Therefore, the column vectors of V are these optimal modes, while the corresponding column vectors of R are the normalised cross-correlation functions. In addition, the squares of the singular values $\sigma_{k}^{2}$ are precisely $\lambda_{k}$ , representing the correlation strength $C_{e}$ between the CCD modes and observable (subject to a discretization constant). In particular, when the components of p that correlate with their corresponding CCD modes of u are of equal energy, $\sigma_{k}^{2}$ also represent the observable-correlated energy of their corresponding CCD modes (subject to a constant), and the correlation ranking is identical to the ranking of the observable-correlated flow energy. In summary, instead of decomposing the flow field u based on its energy ranking using the classical POD, (2.5) yields a decomposition that is based on a cross-correlation ranking with an observable, or the observable-correlated energy ranking in the special case where the correlated components of p are of equal energy. + +Mathematically, the flow decomposition method can be shown to fall under the framework of CCA (Hotelling 1936) as follows. Given two column vectors $\boldsymbol{X} = (x_{1}, x_{2}, \ldots, x_{n})^{T}$ and $\boldsymbol{Y} = (y_{1}, y_{2}, \ldots, y_{m})^{T}$ of random variables with finite second moments, CCA seeks two vectors $\boldsymbol{a} (\boldsymbol{a} \in \mathbb{R}^{n})$ and $\boldsymbol{b} (\boldsymbol{b} \in \mathbb{R}^{m})$ such that the random variables $a^{T} X$ and $b^{T} Y$ yield the maximum correlation. The process may be continued in a subspace to yield a sequence of vector pairs. In the context of CCD, the flow field u may be regarded as the Y vector. However, the key part of the decomposition is to find a proper X vector. There are many ways X can be specified, such as the flow within a specific subdomain of interest. However, the essence and novelty of the present decomposition is to construct an X that consists of the observable sampled in a synchronised manner with the flow but at different time delays. Compared to POD or the extended POD, this time shift is an additional dimension used in CCD. As will be shown, the additional information embedded in this “hidden” shifted-time dimension is the key to yielding a more observable-targeting decomposition. More importantly, this permits independent sampling rates between the observable and the flow, which can be of great advantage. + +CCD possesses a number of key features that would be particularly useful for targeted flow diagnosis. First, the decomposition modes are not ranked by their energy, but by the correlation strength with the observable. Flow features that are not correlated with the observable can be effectively suppressed, while those correlated are promoted and ranked according to their correlation strength with the observable. This targets exclusively the observable and is, therefore, very useful in finding the sources or descendant structures of the observable. Second, as will be shown in section 3, the decomposition is robust even when the signal-to-noise ratio (SNR) is low. This is useful when only a small portion of the flow energy correlates with the observable, for example in the classical problem of aeroacoustic emission due to turbulence. Moreover, this robustness can be continuously improved when a longer time duration is used. This is therefore suitable for experimental diagnosis, where an arbitrarily long measurement may be readily performed. + +Third, as will be shown in section 3, the decomposition appears more capable of order or dimensionality reduction compared to POD. This is because CCD aims to decompose the flow only in the observable-correlated subspace, rather than the entire $\mathbb{R}^M$ . In fact, this fact may be used to estimate the convergence of the decomposition by examining how well observable can be reconstructed only using modes corresponding to non-zero singular values. Last but not least, the flexibility to use different sampling frequencies for the flow and the observable enables one to fully exploit the instrument's capabilities in experiments and numerical simulations. For example, it is well known that acoustic signals can often be sampled much faster using a microphone than the entire flow field using PIV. Similarly, in numerical simulations, the observable can also be sampled much faster than the flow due to limited storage requirements imposed by the observable at only a number of probe positions. Note that in general the sampling frequency of the observable is independent of that of the flow, provided $p_i$ properly aligns with $u_i$ as prescribed in section 2.1. In practice, if the sampling frequency of the observable is an integer multiple of that of the flow, it would be trivial to achieve such alignment. In other cases, clock-triggered synchronisation may be used to meet such a requirement in experiments. + +# 2.3. Frequency and wavenumber resolution and sampling delay + +In section 2.1 we mention that the matrix P is assumed to have Q rows and each adjacent row is shifted by time $\Delta\tau = \tau_{j+1} - \tau_j$ (assuming a constant sampling frequency). Moreover, p is sampled temporally behind u by a time $\tau_1$ (or ahead of u if $\tau_1$ is negative). In practice, the choice of Q, $\Delta\tau$ , and $\tau_1$ has significant physical implications. + +First, we show that $\Delta\tau$ and Q determine the maximum and minimum frequencies that can be resolved in the cross-correlation between the flow and the observable, respectively. To see this, we start by defining the correlation tensor $C(\tau', \tau)$ as + +$$ +C (\tau , \tau^ {\prime}) = \int_ {\Omega} R ^ {*} (\tau , \boldsymbol {x}) R (\tau^ {\prime}, \boldsymbol {x}) \mathrm{d} x ^ {n}. \tag {2.19} +$$ + +Similar to that shown in section 2.2, we can show that after discretization $C(\tau, \tau')$ reduces to $AA^{\dagger}$ subject to a scaled constant. The eigenvalue $\lambda$ defined in (2.15) can also be found by + +$$ +\frac {1}{T} \int_ {\tau_ {0}} ^ {\tau_ {0} + T} C (\tau , \tau^ {\prime}) \psi (\tau^ {\prime}) \mathrm{d} \tau^ {\prime} = \lambda \psi (\tau), \tag {2.20} +$$ + +where $\psi (\tau)$ corresponds to the column vectors of matrix $\pmb{R}$ defined in (2.5) in a discretized form. When the function $C(\tau ,\tau^{\prime})$ is of a homogeneous (stationary) form, i.e. + +$$ +C (\tau , \tau^ {\prime}) = C _ {0} (\tau - \tau^ {\prime}), \tag {2.21} +$$ + +equation (2.20) reduces to a Fourier expansion (Berkooz et al. 1993), i.e. + +$$ +\int_ {\tau_ {0}} ^ {\tau_ {0} + T} C _ {0} (\tau - \tau^ {\prime}) \mathrm{e} ^ {\mathrm{i} 2 \pi f \tau^ {\prime}} \mathrm{d} \tau^ {\prime} = \lambda T \mathrm{e} ^ {\mathrm{i} 2 \pi f \tau}, \tag {2.22} +$$ + +or equivalently, + +$$ +C (\tau , \tau^ {\prime}) = \sum_ {n} \lambda_ {n} T \mathrm{e} ^ {\mathrm{i} 2 \pi f _ {n} (\tau - \tau^ {\prime})}. \tag {2.23} +$$ + +Equation (2.23) indicates that the function $C(\tau, \tau')$ can be expanded into a Fourier series. When $C(\tau, \tau')$ is discretized, the well-known Nyquist's theorem demands that the sampling frequency $f_s^p \equiv 1 / \Delta \tau$ of the observable must be at least twice as large as the highest frequency to be resolved. Similarly, the total sampling duration $Q\Delta \tau$ determines the frequency resolution to be $1 / Q\Delta \tau$ . When the function $C(\tau, \tau')$ is not a homogeneous function, there are no general theorems, but we expect that the frequency requirement remains similar to the homogeneous case. In summary, $\Delta \tau$ determines the maximum frequency while $Q$ determines the frequency resolution similar to those in the Discrete Fourier Transform (DFT). + +Second, we note that the choice of $\tau_{1}$ depends on the physical time delay between p and u. In general, the observable may be temporally ahead of or behind the flow events depending on the causal relations between the two. For example, if p represents the upstream forcing imposed near the nozzle lip of a turbulent jet, then there must exist a finite time delay between the evolved downstream structure and p due to the finite propagation speed of jet instability waves. In this case, p is preferably sampled ahead of u in order to capture the physical correlation within a reasonably short sampling duration of p. A good estimation of $\tau_{1}$ would be around $-d/U_{c}$ , where d and $U_{c}$ represent the maximum distance between the flow and the observable and the convection velocity of the instability waves, respectively. Conversely, if p is temporally behind u then it must be sampled after u. For example, if the observable p represents the acoustic pressure at a distance r from the jet flow, a good estimate of $\tau_{1}$ would be around r/c, where c represents the speed of sound. In other more general flows, a good estimate of $\tau_{1}$ may be obtained by examining the cross-correlation function between the flow and the observable. $\tau_{1}$ should be chosen such that the correlation matrix A captures the entire correlation peaks. + +In addition to the sampling rate, sampling duration, and sampling delay of p, the temporal and spatial sampling of u also have physical implications. First, the spatial sampling rate of u has the conventional implication that it determines the maximum spatial wavenumber, whereas the length of the spatial sample determines the minimum wavenumber that can be resolved. This can be shown in a similar manner to those shown from $(2.19)$ to $(2.23)$ by considering the expansion of $B(\boldsymbol{x}, \boldsymbol{x}')$ . We omit a repetitive presentation here for brevity. + +Second, the temporal sampling rate of the flow $f_{s}^{u}$ (when $u_{i}$ is obtained via temporal sampling), however, has a different implication. The sampling rate here is not to determine the frequency limit, but mainly to ensure the convergence of the correlation between p and u. In particular, there is no need for the flow field and the observable to be sampled at the same frequency. This is an important advantage, because, as discussed in section 2.2, in experiments PIV can only be sampled at a much slower rate than that using a hot-wire or a microphone, whereas in numerical simulations sampling the flow field fast is impractical because of storage limit. However, such limitation does not exist for a number of interested observables. Therefore, the much higher sampling rate of the observable can be fully exploited by CCD in both experiments and numerical simulations. The fact that the sampling rates of the flow and observable are independent of each other is evident in the case that $u_{i}$ is obtained in the ensemble space. + +# 2.4. Inclusion of multiple observables + +In many applications, the appropriate observable is not necessarily limited by one. For example, to examine the dominant flow structures in a subsonic round jet that generates sound at $90^{\circ}$ to the jet centreline, the acoustic pressure at any azimuthal position is an appropriate choice due to the azimuthal statistical homogeneity. In such cases, upon defining a local coordinate system, each observable and the flow field in the local coordinates may be treated as an independent realisation. In such cases, using multiple observables is trivial by following section 2.1, i.e. allowing $u_{i}$ to be sampled both in the temporal and ensemble space. By doing so, the number of flow snapshots is increased by $N_{rl}$ fold, where $N_{rl}$ denotes the number of independent realisations. This would be very useful in improving the convergence of the resulting CCD modes. + +In cases where there is no apparent statistical homogeneity in the flow, multiple observables may still be included. For example, when a turbulent jet is forced in an upstream position (Crow and Champagne 1971), the introduced disturbance evolves downstream. One may wish to extract the coherent structures induced by the forcing using observable measurements downstream of the jet. In such cases, velocity fluctuations at any location within a reasonable distance from the forcing location may be used. However, each observable is likely to be heavily contaminated by turbulence. Using multiple observables are expected to improve the converge of the resulting modes. In such case, suppose that the matrix $P_{i}$ ( $i = 1, 2, 3 \ldots L$ ) can be formed using the i-th observable according to (2.3), then a straightforward way to include multiple observables + +is to form the total matrix P such that + +$$ +\boldsymbol {P} = \left[ \begin{array}{l} \boldsymbol {P} _ {1} \\ \boldsymbol {P} _ {2} \\ \dots \\ \boldsymbol {P} _ {L} \end{array} \right]. \tag {2.24} +$$ + +The normalisation constant Q in $(2.4)$ should be replaced by LQ. However, it is important to note that although P has L times as many rows as $P_{i}$ , this does not improve the temporal frequency resolution of the decomposition, which is still determined by $P_{i}$ . This is because, as illustrated in section 2.3, the temporal frequency resolution is determined by the duration of the time shift $Q\Delta\tau$ when $(2.8)$ is truncated and discretized; including more observables does not increase the length of this duration. Nevertheless, convergence of the resulting CCD modes may improve due to the effective inclusion of more data, particularly when highly noisy observables are used. For highly complicated flow data with a limited sampling duration, such as those obtained in numerical turbulent simulations, including multiple observables is expected to improve the convergence, i.e. reduce the uncertainty or noise of the resulting CCD modes. + +The choice of multiple observables, non matter in statistical homogeneous or inhomogeneous flows, must be made with care. As mentioned, the observables must be expected to resolve the same structures either due to statistical homogeneity or well-defined sources of the underlying problem. In the case where the multiple observables chosen are correlated with different events, including more observables would effectively seek an average between these flow structures, which may not be one's intention. For example, if one is interested in identifying the flow structures that are most correlated with the skin friction under a turbulent boundary layer, observables sampled at various streamwise stations are expected to resolve different structures. In such cases, using multiple observables may not be a worthwhile technique. + +# 2.5. Connection to POD and extended POD + +As shown in section 2.2, CCD is different from POD in that the decomposition is based on a cross-correlation rather than an energy norm. This difference is similar to that between CCA and its sister method PCA in classical statistics. Physically, CCA aims to find the “common parts” between two sets of variables, while PCA aims to find the main energetic structures. Mathematically, instead of decomposing the matrix $U^{\dagger}$ , a projection onto P is performed first in CCD. This shows that the decomposition takes into account the space spanned by P. Note this projection may result in a rank that is lower than that of the original flow; however, this is intended as one seeks to decompose $U^{\dagger}$ in the subspace correlated with the observable only. One could argue that this projection leads to a “lower-rank” behaviour by construction, as this would yield fewer singular values. However, the low-rank behaviour we discuss in the following sections is not characterised by fewer singular values, but rather characterised by a quick decay of singular values as the mode number increases and, perhaps more importantly, by a rapid reconstruction of the observable using fewer flow modes. + +As mentioned in section 1, the extended POD (Maurel et al. 2001; Borée 2003) is developed with a similar aim as the present decomposition, i.e. to better target the observable. One can show that the extended POD using a subdomain s is closely related to the degenerate case of CCD when no time shift is allowed between the observable and flow (using multiple observables in s). Mathematically, this implies Q = 1, $\tau_{1} = 0$ and the observable matrix P shown in (2.3) is a degenerate row vector of rank 1. In the special case where the subdomain of the extended POD only includes one observable point and only one mode results, the extended POD and degenerate CCD are identical subject to a normalisation constant. This can be shown as follows. + +Suppose that there exist L observables in the subdomain s. Since no time shift is allowed between the flow and observable, the matrix $P_{i}$ for each observable is a row vector. Hence, the assembled matrix P is a matrix of dimension $L \times N$ . Written in the matrix convention used in the present paper, the essential steps of the spatial extended POD start by decomposing P using POD or, equivalently, by SVD, i.e. + +$$ +\boldsymbol {P} ^ {\dagger} = \boldsymbol {R} _ {s} \boldsymbol {\Sigma} _ {s} \boldsymbol {V} _ {s} ^ {\dagger}, \tag {2.25} +$$ + +where both $R_{s}$ and $V_{s}$ are unitary matrices, the subscript s represents that this is a POD performed in the subdomain s. Note that it is the $P^{\dagger}$ that is decomposed. Right-multiplying (2.25) by $V_{s}$ , one obtains + +$$ +\boldsymbol {P} ^ {\dagger} \boldsymbol {V} _ {s} = \boldsymbol {R} _ {s} \boldsymbol {\Sigma} _ {s}. \tag {2.26} +$$ + +Taking the $k$ th column of both sides of (2.26) yields + +$$ +\boldsymbol {P} ^ {\dagger} \boldsymbol {V} _ {s, k} = \boldsymbol {R} _ {s, k} \sigma_ {s, k}, \tag {2.27} +$$ + +where $\sigma_{s,k}$ represents the kth diagonal element of $\Sigma_{s}$ . The right-hand side of (2.27) represents the temporal coefficient of the kth subdomain POD mode $V_{s,k}$ . The kth extended POD mode $V_{e,k}$ is obtained by projecting the flow U in the extended domain defined in (2.1) onto the kth temporal coefficient, followed by a normalisation, i.e. + +$$ +\boldsymbol {V} _ {e, k} = \frac {1}{\sigma_ {s , k} ^ {2}} \boldsymbol {U} \boldsymbol {P} ^ {\dagger} \boldsymbol {V} _ {s, k}. \tag {2.28} +$$ + +Following the procedure introduced in sections 2.1 and 2.4, the multiple-observable CCD yields + +$$ +\frac {1}{N \sqrt {L}} \boldsymbol {P} \boldsymbol {U} ^ {\dagger} = \boldsymbol {R} \boldsymbol {\Sigma} \boldsymbol {V} ^ {\dagger}, \tag {2.29} +$$ + +where R, $\Sigma$ and V are defined earlier in section 2.1. Left-multiplying (2.29) by $R^{\dagger}$ and then taking the Hermitian adjoint of both sides of the resulting equation yields, + +$$ +\frac {1}{N \sqrt {L}} \boldsymbol {U} \boldsymbol {P} ^ {\dagger} \boldsymbol {R} = \boldsymbol {V} \boldsymbol {\Sigma}. \tag {2.30} +$$ + +Taking the kth column of both sides of $(2.30)$ yields the kth multi-observable degenerate CCD mode + +$$ +\boldsymbol {V} _ {k} = \frac {1}{N \sigma_ {k} \sqrt {L}} \boldsymbol {U} \boldsymbol {P} ^ {\dagger} \boldsymbol {R} _ {k}. \tag {2.31} +$$ + +Comparing (2.28) and (2.31), one sees that the kth extended POD and degenerate CCD modes share much similarity. In particular, since both $V_{s}$ and R are unitary matrices of size $L \times L$ , $V_{s,k}$ and $R_{k}$ are of similar forms. This shows that both modes can be written as a projection of $UP^{\dagger}$ onto a unitary matrix of the same size. However, since $V_{s}$ is obtained by decomposing $P^{\dagger}$ while R by decomposing $PU^{\dagger}/N\sqrt{L}$ , in general, they are not the same. This represents the key difference between the two methods, i.e. one uses an energy-like rank in the subdomain only, while the other uses a correlation rank involving both the subdomain and full domain. It is also this difference that ensures the resulting CCD modes are orthogonal, while it is not necessarily so for the extended POD. + +However, in the special case where only one observable exists in the subdomains s and only one extended POD mode results, both $V_{e}$ and R reduce to 1. Clearly, in this case, the kth extended POD and degenerate CCD modes are identical, subject to a normalisation constant. This also suggests that a key difference between the two is that an extra dimension of time shift is allowed in CCD. It is in fact this difference that results in a more effective order-reduction, which will be discussed in the following sections. + +In summary, one can see that CCD is different from the extended POD in the following ways. First, CCD uses a norm involving both the subdomain and full domain, while the extended POD uses a norm defined in a subdomain space. Second, it is not the energy of the flow within a subdomain that is maximised, but the cross-correlation between the flow and the observable, which is the key difference from the extended POD. Last, the matrix P is formed by consecutively shifting the temporal delay between the flow and the observable. This is why although $A^{\dagger}A$ can be written as $U(P^{\dagger}P)U^{\dagger}$ , CCD is not weighted POD as $P^{\dagger}P$ is a non-diagonal matrix formed by time shifting the observable, instead of a diagonal weight independent of the flow variables. Note that P does not have to be within the flow field; instead, it can represent a variable outside the flow field, a Fourier component of the flow, a particular event in a complex flow, or an observable obtained by integrating the entire flow field. + +Apart from these differences, the connections between POD, extended POD and CCD can also be shown. For example, mathematically POD can be regarded as a special case of CCD when the observable is just an impulse exhibiting no spectral preferences. Specifically, if $p_{ij} = \delta_{i(N+1-i)}$ where $i = 1, 2, \ldots, N$ and $\delta_{ij}$ is the Kronecker delta function, we see that matrix A is a reversed $U^{\dagger}$ and CCD reduces to POD. Physically, this implies that p contains identical frequency components, and therefore exhibits no preferences in the spectral space. Therefore, U is decomposed into modes ranked purely by their energy. Similarly, mathematically CCD may reduce to the extended POD if P is a mathematically constructed simple diagonal weight matrix independent of any flow variables. The exact diagonal elements of course depend on the specific subdomain to be interrogated in the extended POD. + +# 3. Validation + +# 3.1. One-dimensional deterministic flow fields + +To validate that CCD can effectively extract flow events that correlate with an observable, even under very low SNR, we create an artificial one-dimensional unsteady flow field + +$$ +\begin{array}{l} u (x, t) = 2 \cos (t - x) + 1. 5 \cos (2 t) \cos (2 x) + \cos (3 t) \cos (3 x) \\ + 0. 5 \cos (4 t) \cos (4 x) + \cos (6 t) \cos (6 x) \exp (- 0. 1 (x - \pi) ^ {2}) + 1 0 0 r (t, x), \tag {3.1} \\ \end{array} +$$ + +where $r(t,x)$ represents a random noise field with a uniform probability distribution over $[-0.5,0.5]$ , while other terms represent given flow structures with different amplitudes. Note that the energy of the random noise field is deliberately chosen to be around $10^{4}$ times stronger than the defined flow structures. + +Suppose that p represents an observable of interest at a specific point of the flow field, for example, it may represent the skin friction fluctuations at one point on the bottom wall within a turbulent channel flow. It is known that some flow structures are the primary cause of the skin friction fluctuations while others have minimal effects on them. Therefore, as an illustration we suppose that p is generated by the flow events represented by the first, second, fourth and fifth terms in $(3.1)$ , but not by the third and last terms. For instance, p may be given by + +$$ +p (t) = \cos (t - \frac {\pi}{4}) + \sin (2 t - \frac {\pi}{3}) + \cos (4 t) + \cos (6 t - \frac {\pi}{1 2}). \tag {3.2} +$$ + +Note that the amplitudes of the terms shown in $(3.2)$ are chosen to be identical, although this is not at all necessary. In fact, they may be changed arbitrarily without affecting the validity of the decomposition, for instance, the amplitudes shown in $(3.1)$ can be used should one be interested. + +Suppose that the flow field u is sampled over $t \in [0, 2N\pi]$ at a sample frequency $f_{s}^{u} = 128/2\pi$ , where N is an integer representing the number of periodic cycles. Given the strong random noise in (3.1), N is chosen to be a large number (only necessary when strong noise is present). p, on the other hand, is sampled at the same sample frequency $f_{s}^{p} = f_{s}^{u}$ but for a slightly longer duration of $2(N + 1)\pi$ . According to section 2.1, by choosing $\tau_{1} = 0$ and Q = 128, we can construct a matrix P with 128 rows straightforwardly. Within each snapshot, the flow field is discretized on a mesh of 128 points uniformly distributed between $[0, 2\pi]$ . In this example, $\Delta\tau = 2\pi/128$ , therefore the maximum frequency that can be resolved is limited by around $64/2\pi$ . Similarly, Q = 128 implying that the frequency resolution is around $1/2\pi$ . + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/d9d99ac379847f175f6232b864e3bc539ae9171bf99fd22936981ae37e7d3c88.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/f35c492029a591bd7abab3e1a8320a1acadd4577449f4895b0ab5b9ed38817b5.jpg) + + + +Figure 2: Comparison of the spectra of CCD (a) and POD (b) when $N = 10^{4}$ , Q = 128 and $f_{s}^{p} = f_{s}^{u} = 128/2\pi$ . CCD is capable of effectively extracting the observable-correlated events leading to a low-rank spectrum, while POD results in a flat spectrum. + + +Following the procedures introduced in section 2.1, both matrix U and P can be easily constructed, where U is of a size of $128 \times 128N$ while P is of a size $128 \times 128N$ . Upon constructing the matrix A, the CCD can be carried out in a straightforward manner. The resulting CCD spectrum, i.e. the magnitude of the singular values against the mode number, is shown in figure 2(a). To facilitate a direct comparison, the POD spectrum is also shown in figure 2(b). + +Figure 2 shows the CCD spectrum with a desired low-rank behaviour. From figure 2(a) we see that the five modes that correlate with the observable can be robustly identified, even though the energy of the random noise is up to $10^{4}$ times stronger. Specifically, the first two identical singular values form a pair, revealing a flow event of travelling-wave nature, i.e. $\cos(t - x)$ . The first mode of the pair corresponds to $\sin(t + \phi)\sin(x + \phi)$ while the other to $\cos(t + \phi)\cos(x + \phi)$ ( $\phi$ is an arbitrary phase delay), as demonstrated in figure 3(a). The third, fourth, and fifth singular values correspond to the flow events described by the second, fifth, and fourth terms in (3.1), respectively. These can be confirmed by examining the corresponding mode vectors shown in figure 3(b-d). Most importantly, the $\cos(3x)$ mode, which does not correlate with the observable, is robustly removed in the CCD spectrum. This shows that CCD can effectively remove those uncorrelated flow events while only keeping those correlated, and therefore works well for an observable-targeted feature extraction and order reduction. + +The sixth to the ninth singular values $(\sigma_{j}^{2})$ shown in figure 2(a), which are two orders of magnitude weaker than the first few modes, are artefacts introduced by the strong random noise. Note, however, that these unphysical modes can be further suppressed robustly if the flow field is sampled for a longer duration (larger N). All other values of $\sigma_{j}^{2}$ are below $10^{-24}$ and therefore not shown within the given range. As discussed in section 2.2, the singular values represent the correlation strengths between corresponding CCD modes and the observable. In this illustrative case, the observable is comprised of four modes of equal amplitude, as shown in (3.2), therefore the singular values in figure 2(a) are precisely the observable-correlated fluctuation energy (subject to a fixed constant), as evidenced in figure 3 (for example $\sigma_{1}^{2}:\sigma_{2}^{2}:\sigma_{3}^{2}=2^{2}:2^{2}:1.5^{2}$ ). + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/f9d4d3688ccfda05ec3b750e581d1b55461d83c70a23f13a277578fc45232eb3.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/09200b6a65af1b7c860c05cc93d12cf46f5519325a13aac274dff76240343715.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/b1274b88dc1871df86a5616ecc6333da8d2b868230695d475db0bb4fcf975e05.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/258c99e1ad45bc9fcf905fef10804234fc32d070203d12f4e0b75d193e1bff73.jpg) + + + +Figure 3: Extracted CCD modes when $N = 10^{4}$ , Q = 128 and $f_{s}^{p} = f_{s}^{u} = 128/2\pi$ . They are most correlated with the observable p (corresponding to the first, second, fifth and fourth terms in (3.2), respectively). + + +On the other hand, figure 2(b) shows that due to the strong random noise the POD spectrum is completely corrupted and shown as a flat line. The low-rank behaviour embedded within the data therefore cannot be identified. This is expected, because POD modes are ranked by their corresponding fluctuation energy. The random noise present in the flow field is up to $10^{4}$ times stronger than the observable-correlated events, and therefore completely dominates the POD spectrum. More importantly, even though POD may be able to extract the coherent structures when weaker noise is present, it cannot separate the observable-correlated flow structures from those uncorrelated in the same way as CCD does, since no information of $p$ is used. For example, the second term of (3.1) would stay in the POD spectrum and also exhibit as a dominant mode. + +Having validated the decomposition, one can straightforwardly demonstrate the effects of varying the sampling frequency, duration, time shift and including multiple observables. The results agree well with the arguments discussed in sections 2.3 and 2.4. For conciseness, however, we do not include them in this section, but rather have it shown in Appendix A. + +# 3.2. One-dimensional statistical flow fields + +The example shown in figures 2 and 3 illustrates the capability of CCD in extracting flow events from highly noisy data. The temporal signals given in (3.1) are deterministic; we can show in a similar manner that CCD can also effectively extract the observable-correlated flow events when the temporal variation is statistical, such as those exhibited in many turbulent flows. To show this, we construct an artificial one-dimensional flow field + +$$ +u (x, t) = 3 s _ {1} (t) \cos x + 2 s _ {2} (t) \cos 3 x + s _ {3} (t) \cos 6 x \exp (- 0. 1 (x - \pi) ^ {2}) + 1 0 r (t, x), \tag {3.3} +$$ + +where $s_{i}(t)$ (i = 1, 2, 3) represent three statistical processes. The $s_{i}(t)$ series are generated by a random number generator with different seeds in MATLAB and then filtered using three different 6th-order Butterworth filters. More specifically, $s_{1}(t)$ is filtered using a bandpass filter with lower and upper cut-off frequencies of $0.2f_{s}$ and $0.4f_{s}$ , respectively. The $s_{2}(t)$ and $s_{3}(t)$ series are filtered using low-pass filters with cut-off frequencies of $0.2f_{s}$ and $0.15f_{s}$ , respectively. For illustrative purposes, we also add a random noise field that is two orders of magnitude more energetic than $s_{3}(t)$ . Suppose that the observable p is generated by the flow events represented by the second and third terms in (3.3), but not by the first, i.e. + +$$ +p (t) = s _ {2} (t - \frac {\pi}{3}) + s _ {3} (t) + 2 \left[ s _ {3} (t) ^ {2} - \overline {{s _ {3} (t) ^ {2}}} \right] + 3 \left[ s _ {3} (t) ^ {3} - \overline {{s _ {3} (t) ^ {3}}} \right] + r (t). \qquad (3. 4) +$$ + +Note that because the observable may be non-linearly related to the flow dynamics, we also add in (3.4) two nonlinear terms of $s_3(t)$ , as shown by the two bracket terms. Similarly, the observable may be also subject to noise contamination. A statistical random noise $r(t)$ , with a uniform distribution over $[-0.5, 0.5]$ , is therefore also added. The flow field is again sampled at $f_s^u = 128 / 2\pi$ on a uniform spatial mesh of 128 points over the time interval $[0, 2N\pi]$ , while $p$ is sampled over $[0, 2(N + 1)\pi]$ using the same frequency $f_s^p = 128 / 2\pi$ . + +Routine use of the decomposition yields the CCD spectrum and the first two modes, as shown in figures 4(a) and 4(b), respectively. Clearly, the leading-order mode corresponds to the second term in (3.3), while the second-order mode the third. This can be clearly seen from figure 4(b). It is worth noting that the observable also contains the square and cube of $s_3(t)$ , but this does not appear to affect the identification of the second mode. Indeed, CCD works by maximizing the correlation between the flow field and the observables, but in general it does not limit the observable being a linear function of the flow field. Additionally, the first term of (3.3), due to it being uncorrelated with $p$ , is effectively removed in the CCD spectrum. Other higher-order modes are more than two orders of magnitude lower than the first two. Again, as $N$ increases, these unphysical modes can be further suppressed, while the physical modes resolved more accurately. Note that in this illustrative example, the observable p is also corrupted by the random noise, but CCD continues to work robustly. + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/03108d0c82d261a4cb2cbcd8380144ae5c5fe14e0bfead9f68c1bce1bbb30360.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/cc91f66662bd8489047a732a67289db47468d885f224eaa3351740b57b16ae86.jpg) + + + +Figure 4: The CCD spectrum (a) and the first and second modes (b). N takes the value of $10^{4}$ but $10^{3}$ may also be used at the expense of convergence. + + +# 4. Applications to numerical and experimental data + +Having validated the method, in this section CCD is used to decompose numerical and experimental data in order to demonstrate its potential use in fluid mechanics. Three flows are used, including a turbulent channel flow, a subsonic jet and a wake flow past a cylinder. Where possible, POD results are also included for comparison. In all cases, the simple $L_{2}$ norm of the flow $u_{i}$ is used in POD. + +# 4.1. Turbulent channel flow + +As an illustrative example, we first apply CCD to a Direct Numerical Simulation (DNS) database of turbulent channel flows. The database was obtained from a turbulent channel flow using the code developed by Lee and Moser (2015). The computational domain is of $4\pi H \times 2H \times 2\pi H$ in the streamwise $(x)$ , wall-normal $(y)$ and spanwise $(z)$ directions, respectively, where H denotes the half-height of the channel. The domain is discretized using 192, 128 and 192 points in x, y and z directions, respectively. The friction Reynolds number $Re_{\tau}$ defined as $\rho u_{\tau}H/\mu$ , where $\rho$ , $\mu$ and $u_{\tau}$ denote the fluid density, dynamic viscosity and friction velocity at the wall respectively, is around 180. The time step is fixed at $0.01H/U_{b}$ , where $U_{b}$ is the bulk flow velocity. The flow is sampled every 100 time steps, resulting in a sampling frequency of $f_{s}^{u} = U_{b}/H$ . In total, 1687 snapshots of the flow field are recorded. + +In turbulent channel flows, skin friction represents a significant operational cost in applications such as long-range oil transport (Kim 2011). The control of turbulent skin friction is therefore of particular interest and has been studied extensively in the literature (Gad-el Hak 2007). To understand the physical mechanism concerning its generation and suppression, it is crucial to extract the turbulent flow structures that determine the skin friction. CCD is therefore suitable for such a diagnosis. As mentioned in section 2, without the data storage limit, the observable is allowed to be sampled at a much higher frequency than the flow field. In this example, the sampling frequency $f_{s}^{p} = 10f_{s}^{u}$ , resulting in an interval of $\Delta\tau = 0.1H/U_{b}$ and 16870 samples for the skin friction. + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/55f0684256957b3b9f104ebd3d7e303e68f8cab3d6ecb15bacc7ca3ae231abed.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/bc88a20258ee3a01741790eccc7dac177e4799bea7e6b3cba953fa9df6fa6e21.jpg) + + + +Figure 5: The spectra of (a) CCD and POD; The CCD spectrum exhibits a much steeper decay as mode number n increases, indicating a more effective order reduction. + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/b9b11fc53d3a151dd7cdb4d517d89357530d251cc44def4400b002a38cefbaba.jpg) + + + +Figure 6: Reconstruction of the dimensionless wall friction coefficient using the first 6 CCD (a) and POD (b) modes, respectively. + + +Considering the statistical homogeneity of the flow, we use the skin friction sampled at $x = 2\pi H$ and $z = \pi H$ on the lower wall (y = -H) as the observable and choose the streamwise velocity as the flow variable in order to extract the coherent structures. Considering the short temporal correlation scale, we choose Q = 100 and $\tau_{1} = -50\Delta\tau$ . Using the procedures described in section 2, we perform CCD and obtain the resulting singular values and CCD modes. The singular values are shown in figure 5(a). Also shown is the spectrum from POD in figure 5(b), where the streamwise velocity is decomposed. Comparing the two we see that the CCD spectrum is markedly different from that of POD. In particular, the CCD spectrum exhibits a much quicker decay. For example, higher-order modes ( $\geqslant$ 5) are one order of magnitude lower, whereas the POD spectrum is rather flat. This signals a quicker reconstruction of the skin friction using CCD modes. Indeed, using the first 6 modes recovers more than 80% of the total skin friction at the observer point, as shown in figure 6(a). The high-frequency deviation may be further reduced if the observable is allowed to be sampled faster. In contrast, the first 6 POD modes only recover less than 5% energy, as shown in figure 6(b). + + +B. Lyu + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/865b6a2a8053c9fd40cf26474029139a6d7700d1b997182ef4a11856bdbb8704.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/bbbcb6b7099211517e4dc62668f088b5681ad9a76e8939cb70b6df3a3075a2ce.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/c8d69272b6b20fe9fd0c5e998dbaad05f4a934f818ebd147a29ba299da6b981d.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/3765fe6ce327f660150ff8d16fe72570f3dc43ba6f68c434e5b40ae928b8ad30.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/b1963d6769f79ff07fa864527a14c703e2ed10e8d79157056a288fa73015dfc4.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/cf82d01dca62fcd8a0abfcb8ab08ec8cbd65d4f9f734a991072395c125cb6d23.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/fc1497e2afc7993ff3384bd9456d0494f9833275398ac335f2a6e6d682a9684b.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/a07cc2eb31bc18262172a5dc9a7cf33ea065c59858e98e8f75c55f78c7b10cbf.jpg) + + + +Figure 7: Front views of the first four CCD (a,c,e,g) and POD (b, d, f, h) modes. The z coordinate is fixed at $\pi H$ for (a, c, e, g). The spanwise widths of the streaks in (a,c,e,g) are around 0.2H. + + +The resulting CCD modes are shown in figure 7. We see that the CCD modes take the form of streamwise streaks slightly above the bottom wall, in accordance with current understanding. More importantly, figure 7 also shows that they are spatially localized around the observer point. This is particularly true in the spanwise direction with a streak width of less than 0.2H. Moreover, higher-order modes have increasingly short spatial and temporal scales. To the best knowledge of the authors, such a quantitative and unambiguous characterization of these structures specifically targeting the skin fluctuation in the middle of the wall has not been reported in the literature. In contrast, although the POD modes take the form of streaks, they are not localized around the observer point, but stretched in the streamwise direction and scattered in the spanwise direction instead. Moreover, the first few modes do not exhibit a clear decrease of either spatial or temporal scales, signalling a slower reconstruction of the skin friction. + +Although CCD focuses on examining the flow structures that contribute to the skin friction at one individual point, it in fact does not lose generality. This is because the flow is homogeneous in the streamwise direction; the structures that generate the skin friction at other locations on the wall remain identical (subject to a shift in space). However, by focusing on the observable at a specific point, one expects to obtain a more effective order reduction since our interest is more focused. The fact that the flow is homogeneous can also be exploited to improve the convergence of the resulting flow. Instead of using the observable at one point, one can use multiple points along different spanwise or streamwise locations. They can be treated as independent realisations, with which the resulting mode indeed converges better. However, since the structures remain similar to those in figure 7, we omit showing their contours repetitively. + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/f1e07351702f169b80320f5082cf7cde6dde1e8aebaa08d8dd15fa3d0bc004fd.jpg) + + + +Figure 8: The spectrum of extended POD using (a) a subdomain of size $4\pi H \times 2\pi H$ and (b) a subdomain consisting of the observable point only; Reconstructed skin friction fluctuations using (c) a subdomain of size $4\pi H \times 2\pi H$ (using the first 6 extended POD modes) and (d) a subdomain of only one point (only 1 mode results). + + +Note that part of the reason why the skin friction reconstruction using POD is slow is due to its use of energy within the entire domain as the norm. Since the extended POD can be used to target more towards the observable, it is interesting to compare it with CCD in detail. To show this, we first perform the extended POD using skin friction on the wall. The resulting singular values and reconstruction of the skin friction using the first 6 extended POD modes are shown in figures 8(a) and 8(c), respectively. The resulting spectrum of singular values exhibits a similar slow decay to that shown in POD. This is consistent with a similar reconstruction of the skin friction, as shown in figure 8(c), where a limited time range from 650 to 1150 is shown for clarity. However, comparing to figure 6(b), the skin friction reconstruction appears slightly improved when the wall shear stress is used as the subdomain in the extended POD. + +One is, therefore, interested in seeing how much the reconstruction can improve by using increasingly small subdomains centring around the observable. In the ultimate case, the subdomain can be chosen to consist of the observable point only. We choose to perform extended POD using such a special subdomain. Note that this is identical to the degenerate CCD where no time shift is included between the observable and the flow. We expect the resulting mode to better target the observable, which is indeed the case, as shown in figure 8(d). The extended POD modes can capture an overall trend in the skin friction variation. However, it is important to note that since there is only one mode available, as can be seen in its spectrum shown in figure 8(b), this is the best reconstruction one can achieve using the extended POD. + +On the other hand, since the decomposition is also a degenerate case of CCD, this represents the worst reconstruction one would obtain using CCD. Indeed, by including the dimension of time shifts, P would have a rank of more than 1, and the reconstruction using CCD improves considerably, as shown in figure 6(a). Note that the reconstruction further improves as one includes more CCD modes, the family of which forms a complete orthonormal set. In addition, figure 8(d) shows that only an overall trend of the skin friction is captured in the reconstruction, and the deviation occurs mainly in the high-frequency regime. This is expected, since this degenerate CCD corresponds to a sampling interval $\Delta\tau = \infty$ for the observable, hence a failure to resolve high-frequency components. + +In summary, using only one point where the observable is located in the extended POD better targets the observable, but the resulting one mode limits the capability of separating multiple flow structures that possibly coexist within the flow. To do that, a sufficiently large region is preferred, compromising the observable specificity. This appears a trade-off between targeting a local observable and separating multiple flow structures. CCD does not have this limitation, and this relaxation is enabled by exploiting the “hidden” time-shift dimension. This reflects a key difference between CCD and the extended POD. More importantly, this also adds the flexibility of fully exploiting a different (possibly much higher) sampling frequency. + +Figures 5 and 7 show that CCD works well in extracting the coherent structures that are most correlated with the given observable. This is further evidenced by a quick reconstruction of the skin friction using the first few CCD modes. Note again that in this example the observable is sampled at a much higher frequency than the flow. This flexibility, as mentioned in section 2.5, plays an important role in the successful feature extraction and order reduction. + +# 4.2. Turbulent subsonic round jets + +In this section, we apply CCD to a numerical dataset of a turbulent subsonic round jet. Using the pressure fluctuations as the observable we can examine the flow structures that are most correlated with them. Directly resolving far-field pressure fluctuation is rarely possible in numerical turbulence simulations, hence in this example we examine the near-field pressure fluctuation instead. This is expected to suffice for the purposes of demonstrating the potential use of CCD. The near-field dynamics of turbulent jets is expected to connect with their mixing and acoustic characteristics, and is therefore studied extensively in the literature. Order reduction techniques are widely used. This includes POD or the extended POD with a variety of norms (Freund and Colonius 2009; Sinha et al. 2014; Schmidt and Schmid 2019), the resolvent/input-output analysis (Jeun et al. 2016; Pickering et al. 2021; Bugeat et al. 2024) and other source identification methods that we do not aim to show exhaustively. Moreover, the near-field pressure fluctuations are crucial in determining installed jet noise (Lyu et al. 2017; Lyu and Dowling 2019), therefore its modelling and control have practical uses. + +The numerical data is extracted from an earlier work (Lyu et al. 2017), where an LES simulation of a subsonic round jet was performed. Only a slice of data on one azimuthal plane is used, but it should be sufficient for illustration purposes. The jet Mach number is $M_{j} = 0.5$ while the nozzle diameter D is 2 inches. The computational domain is axisymmetric, with the streamwise coordinate x extending from 0 to 20D and lateral coordinate r extending to 4D. The computational domain is discretized using 512 and 97 points in the x and r directions, respectively. + +We choose the near-field pressure fluctuation at x/D = 10 and r/D = 4 as the observable. At this close distance, the observable is likely to include both acoustic and hydrodynamic pressure fluctuations. In addition, we choose the pressure field as the flow variable to be decomposed. The same is used in a reference POD decomposition. The flow is sampled at a frequency of $f_{s}^{u} = 4U_{j}/D$ for a duration of $200D/U_{j}$ , where $U_{j}$ is the jet exit velocity. The near-field pressure p is sampled at the same frequency but for a longer duration of $280D/U_{j}$ . This results in a Q value of 320. Due to the short distance between the flow field and the near-field pressure fluctuations, we choose the time delay $\tau_{1}$ to be 0. With the procedure described in section 2, the CCD spectrum is shown in figure 9. Also shown is the POD spectrum to facilitate a direct comparison. Only the first 50 singular values are shown. Compared to POD, the CCD spectrum exhibits a more rapid decay as the mode number n increases. In particular, at small mode numbers the CCD spectrum shows a clear low-rank behaviour. The first two modes are almost one order of magnitude stronger than higher-order modes. This is in direct contrast to the POD spectrum, where the low-rank behaviour is not pronounced. + + +B. Lyu + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/ed2416630caf8a7e633240660706c8c2ad2e36cbf4cf8f3664ab743eeb40cf65.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/94a35f6dbc30c508188e1c78404cbb7a4910b8f5645e0af80ab78ff5ebecdf7d.jpg) + + + +Figure 9: The CCD (a) and POD (b) spectra of the unsteady pressure field on a x - r plane. The CCD spectrum shows a clear low-rank behaviour compared to POD. + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/d57282eccc17d2a7b74668be55a1b7f86842ab13ee4ae7bd7713ec82255ebb0d.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/036d397805145727f48ebb633d3fce4c3129f2524771dae27ba396b1f98ffd58.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/ced6f482a7ba46d53a669ae5161a52ee018c56531d8bfc9927040c574c46d8b6.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/f761b2a0c7920391eca6a623ed35c3eaacec42da3d1440aa963937f957af81f4.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/6b83a3ed648ef0aa2e737b3dd91f4fa6d0a2896d96ce7c16e9fea22017ca34f8.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/b740daf0a2daf061f4534a66918c53e44a7f8aeac9a5e829d61d310b4a8577b5.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/f3320ef81e4f61036125a3eb030a3340bbd69460b79610d299632a2750b3d189.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/6c3a17a7d8e35014acc0c1134de9f0e1b1018259f04ed633719934041ffdf95d.jpg) + + + +Figure 10: The first 4 CCD modes (a,c,e,g) and PSD spectra (b,d,f,h) of their corresponding temporal coefficients, where the blue dashed line in (b) represents the spectrum of the observable with its magnitude scaled for an easier comparison; mode 1 (a-b), mode 2 (c-d), mode 3 (e-f), mode 4 (g-h). As the mode number increases the CCD modes are characterised by increasingly short spatial scale and high frequency components. + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/7a72190498f60f37d6494d3a3e264f9d7387ba5a002145bdbb3fd1e4fb2ce03f.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/c221a5bcbe34f4d0f29f6420c226ba099a584b986859c4c5f47862f01c463732.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/8bec60aae520fae10155c2ace2f740d71f39445b17c9b9579d50430061c49e4f.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/2411eaac70dabfed7f0b19c75eedb3384dc989d6e04e0e51a7703a9c40ac0719.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/247f44018ba1c3b65f2334d08e9473fb617ae4a2a2584038c9ac959a29ca26b4.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/d8d9afc59f4e684a99aebb5f9425361596ead54fa8872e0d85da07d913db547c.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/88bbc97f19feb01f3c9a2ad364a03a1b57702c845339de2db15dae422dcd8f44.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/57ea1777cce4c3a019a732d1be9ac8f5d6c3e1e7baf1cefab566d3bca99c76b0.jpg) + + + +Figure 11: The first 4 POD modes (a,c,e,g) and PSD spectra (b,d,f,h) of their corresponding temporal coefficients, where the blue dashed line in (b) represents the spectrum of the observable with its magnitude scaled for an easier comparison; mode 1 (a-b), mode 2 (c-d), mode 3 (e-f), mode 4 (g-h). As the mode number increases the POD modes have larger spatial scales with more low-frequency components. + + +This can be understood from figures 10 and 11, where the first 4 CCD and POD modes $\phi_{k}(\boldsymbol{x})$ and the corresponding Power Spectral Densities (PSDs) of their temporal expansion coefficients $a_{k}(t)$ are shown, respectively. Clearly, the first two CCD modes are large flow structures exhibiting relatively low-frequency behaviour, whereas the leading-order POD modes have much shorter scales with a well-known dominant frequency at around St = 0.3, where St is the Strouhal number defined using $U_{j}$ and D. The PSD spectrum of the observable is also included with its magnitude scaled for an easier comparison. Since the observer is located at x/D = 10 and r/D = 4, the pressure fluctuations inevitably include the signatures of the downstream large coherent structures. The similar first two singular values shown in figure 9 and similar mode shapes shown in figure 10 indicate a convection behaviour of this large structure. CCD decomposition can take this into consideration and yield an observable-relevant low-frequency fluctuation mode. The leading-order POD modes, on the other hand, are ranked only by the fluctuation energy and, therefore, are not as relevant as the CCD modes. + +Note that the singular value represents a measure of the cross-correlation in the $L_{2}$ norm and, therefore, is in general not equal to the energy of the CCD modes contained in the flow, nor is it equal to the energy of the corresponding correlated component in the observable. Nevertheless, since the decomposition targets more at the observable, we expect that it can reconstruct the pressure fluctuations at the observable position using much fewer modes. This is indeed the case, as shown in figure 12, where only the first two CCD and POD modes are included to calculate the reconstructed pressure fluctuations at the observable position, respectively. As can be seen from figure 12(a), using two CCD modes can yield a good reconstruction, which is in contrast to POD shown in figure 12(b). Note that in this application we use the pressure field as the flow u, whereas in general a combined velocity and pressure field may be used. We can show that a similar result may also be obtained when a combination of velocity and pressure fluctuations is used in CCD. + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/685524a0e5480890de753d757ae531177ee58edca6910bd11b3d94cc9b2ade0c.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/11302fcaa990840f7041e3ef018434e6c9429b914919cb51afa1b7ddb1d6e049.jpg) + + + +Figure 12: The reconstructed pressure fluctuations at the observable location using the first 2 CCD (a) and POD modes (b), respectively. + + +At large mode numbers, the CCD spectrum shows a steeper decay, and higher-order modes tend to have increasingly short scales together with higher frequencies, as shown in figures 9 and 10, respectively. Note that the singular values represent the correlation strength between the CCD modes and the observable, therefore the decay of the singular values is determined by both the energy of the flow and the observable and the coherence decay between them. Therefore, the steeper CCD spectrum suggests that although the pressure fluctuations consist of energetic structures of various scales, they may not be equivalently important in contributing to the observable, therefore the coherence between the two may decrease rapidly. On the other hand, the POD spectrum decays much more slowly, and as the mode number increases the POD mode starts to capture more downstream large structures with more low-frequency content, as shown in figure 11. That the POD spectrum decays more slowly is attributed to the fact that the POD spectrum is determined solely by the energy of flow and, therefore, does not depend on its coherence with the observable. + +Figure 10 shows that each CCD mode corresponds to a unique temporal variation. + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/7e9e4ba9cb744e735a908a00db57c49110b20db75285ab7279fb10f02d6cfaf9.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/6155e91309e045c5be247e3aaf27416a9c829c8a60b927a595b5cab862f3690b.jpg) + + + +Figure 13: The mean (a) and instantaneous (b) streamwise velocity distributions of the immediately downstream wake over a cylinder flow (Renn et al. 2023). The black diamond, red circle and white square are located at x/D = 1.5 but y/D = 6.1, y/D = 5.7, and y/D = 4.9, respectively. + + +Unlike the Fourier analysis, each of these temporal variations is spectrally broadband. In essence, CCD decomposition works as a special spectral transform of the flow based on its correlation with the observable. Note, however, that the sampling frequency and duration are limited in this simulation, and further analysis using longer samples is needed for better statistical convergence. In addition, due to current data availability, we only decompose the near-field pressure, and it would be interesting to apply this technique to extract acoustically dominant flow features in future studies. Nevertheless, it suffices for the purpose of demonstrating the potential application of CCD. + +# 4.3. Unsteady wake flows over cylinders + +In this example, we apply CCD to the experimental data of an unsteady wake flow behind a cylinder. The experiment was performed in a water tunnel using the two-dimensional time-resolved Particle Image Velocimetry (PIV) technique. The cylinder had a diameter of $D = 9.53 \mathrm{~mm}$ while the Reynolds number was fixed at 650. The interrogation window was a rectangle immediately behind a cylinder in the wake and measured $13D \times 9D$ in the streamwise ( $x$ ) and cross-stream ( $y$ ) directions, respectively. Details of the experimental setup can be found in Renn et al. (2023). The velocity field was sampled at a frequency of around $50 \mathrm{~Hz}$ on a mesh of $N_x = 133$ and $N_y = 89$ , and in total $N = 8250$ snapshots were obtained. The mean and instantaneous streamwise velocity fields are shown in figure 13 for reference. As shown in figure 13, the mean flow exhibits the expected symmetry across the wake, while the instantaneous velocity field shows a clear vortex shedding behaviour behind the cylinder. The vortex shedding occurring when the Reynolds number exceeds a critical number is one iconic feature of the flow over cylinders. Given its wide applications such as wind blowing over chimneys and high-rise buildings, its control has attracted significant attention in the fluid mechanics community (Choi et al. 2008). Many techniques exist, including both passive and active controls. Earlier studies show that using the feedback signal recorded in the wake, vortex shedding can be successfully suppressed or even eliminated at low Reynolds numbers (Williams and Zhao 1989; Roussopoulos 1993; Park et al. 1994). In designing a closed-loop active control system such as the one in Park et al. (1994), one primary interest is to identify the optimal location to place the feedback sensor. Ideally, the observable, such as the cross-stream velocity, at the feedback sensor location should maintain a strong correlation with the vortex structures shed from the cylinder. CCD may be used to give an initial assessment of the correlation between the sensed signal and the vortex structures. + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/1e1e17728ddad124f470cc584db2d0c76cf0b1231ed9da2713c82436aedb7efc.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/c024dffea9e961154e7f4b43ddfdf7a0b8f95d0148bb740e5be0d18c7112692c.jpg) + + + +Figure 14: The CCD (a) and POD (b) spectra of the streamwise velocity fluctuations on an x-y plane with the cross-stream velocity fluctuation at y/D = 5.7 as the observable. + + +As an illustration, we choose the observable to be the cross-wake velocity (Park et al. 1994) in the initial shear layer behind the cylinder, for example at x/D = 1.5 and y/D = 5.7 as shown by the red circular dot in figure 13(a). Using this observable, we may decompose the streamwise velocity field using CCD. Again, the streamwise velocity is correspondingly used in POD. The time shift $\tau_{1}$ is chosen to be $-4Q\Delta\tau/5$ while Q is chosen to be N/3. Figures 14(a) and (b) show the CCD and POD spectra, respectively. Clearly, both CCD and POD capture the dominant vortex shedding behaviour, and the two nearly identical singular values reflect a convecting behaviour of the shed vortices. Figures 15 and 16 show the corresponding first three CCD and POD modes and their corresponding PSDs, respectively. Clearly the first two vortex shedding modes from both CCD and POD are virtually identical, which can be seen from both the mode shape and their corresponding PSD spectra. The CCD spectrum shows a slightly smaller singular value for the third mode, which is somewhat more symmetric, whereas the similar mode resulting from POD obtains a similar singular value compared to the leading-order mode. This suggests that although this mode carries one of the largest energy, it is slightly less correlated with the cross-stream velocity fluctuation at x/D = 1.5 and y/D = 5.7. + +If we keep x/D = 1.5 but move the observable position further away from the shear layer, for example, to y/D = 4.9 and y/D = 6.1 as shown by the white square and black diamond symbols respectively in figure 13, these three modes can still be identified using CCD, but their relative singular values changed significantly, as shown in figure 17. This implies that these modes correlate differently to different observables. Evidently, the first and second modes in figure 17(a) represent the vortex shedding modes. Their mode shapes are similar to those shown in figure 15(a) and (b), so we omit a repetitive presentation. + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/f7bbbafa2f5d037c742b95e57b95063ea8544e4141145c01b79935f8576aec65.jpg) + + + +Figure 15: The first 3 CCD modes (a,c,e) and PSD spectra (b,d,f) of their corresponding temporal coefficients; mode 1 (a-b), mode 2 (c-d) and mode 3 (e-f). + + +However, it is important to note that these singular values are much larger compared to those shown in figure 14(a), suggesting they are more strongly correlated with the observable. More importantly, the third singular value drops rapidly, almost one order of magnitude weaker than the leading-order mode. From the feedback control point of view, this would be a good candidate for placing the feedback sensor owing to its higher correlation with our interested flow events and simultaneously a higher SNR. Figure 17(b) shows that the singular values corresponding to the vortex shedding modes are slightly lower than those shown in figure 14(a) with an even stronger leading-order non-shedding mode. Consequently, this would be a position to be avoided for placing the feedback sensor. This may be why the wake centreline was used to place the feedback sensors in Park et al. (1994). + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/4e131b3f4172e290869bc6016058fde48507d4a60d8b65427485e11baaf1f530.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/87d501398c09037834baf561db4b80a2662de7b7843cae3d2209904fc91a6a26.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/26e9f993057fd4755b21ccfe2773934221e1ae5e0e4f7650055c46c25dab733b.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/bac251285e48233274d6c0fc6e5cbeb2076ac83519056848054165a5cb9f3af7.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/825b65945aa9e4958c4b07409d471cccf8729f26d8d600301514f90c69845c1d.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/2d508ae0d4f1abc16dd0666d7a42d95eda390d8134965cd509d7318d69cf097f.jpg) + + + +Figure 16: The first 3 POD modes (a,c,e) and PSD spectra (b,d,f) of their corresponding temporal variation coefficients; mode 1 (a-b), mode 2 (c-d) and mode 3 (e-f). + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/6f2fecafe70c5574de69b22829965a75b748cd4d373bd72d2e4e6abf3dbd5473.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/c47f038310a2071c4a49a9f662ea1ced7e79a0997f56d497029ab36e3cd47f80.jpg) + + + +Figure 17: The CCD spectra of the streamwise velocity fluctuations when the observable is located at $y / D = 4.9$ (a) and $y / D = 6.1$ (b), respectively. + + +# 5. Conclusion + +A data-driven method referred to as CCD is proposed in this paper to decompose complex flows into modes ranked by their correlation strength with an observable. The method is based on the canonical correlation analysis in classical statistics. The method is validated for both deterministic and statistical flow events. First, the results show that CCD can effectively extract the observable-correlated flow features while suppressing those uncorrelated in both cases. CCD, therefore, results in more low-rank spectra compared to POD. Second, CCD can effectively extract those observable-correlated flow structures even under low SNRs. Third, numerical validation shows that the sampling frequency and duration of the observable determine the frequency limit and resolution while that of the flow are to ensure the convergence of the cross-correlation. Longer sampling of the flow and including multiple observables can improve the convergence of the resulting CCD modes. Therefore, CCD is particularly suitable for experimental data because long samples can be more conveniently obtained. Lastly, as no linearity is assumed, CCD is capable of extracting nonlinear flow events similar to POD, provided a non-negligible correlation exists between the flow and the observable. + +As an illustrative example, the method is first used to analyse a turbulent channel flow obtained using DNS. The flow structures that are most correlated with the skin friction at the point in the middle of the bottom wall are extracted. It is shown that CCD yields a spectrum of singular values that decays rapidly as the mode number increases compared to POD. The first 6 CCD modes effectively recover more than 80% of the skin friction fluctuations. The extended POD using only one observable point can better target the observable, and is found to be equivalent to the degenerate case of CCD when no time shift between the flow and observable is used. The CCD modes take the form of streamwise streaks slightly above the wall. More importantly, the streamwise and spanwise extent of these streaks are unambiguously determined. As the mode number increases, CCD modes have increasingly short spatial and temporal scales. + +In a subsequent example, CCD is used to decompose the unsteady pressure field of a turbulent subsonic jet using a near-field pressure fluctuation as the observable. Results show that CCD results in a steeper spectrum compared to POD. In particular, the CCD spectrum exhibits a clear low-rank behaviour and the corresponding modes correspond to the large coherent flow structures that convect downstream. The first two CCD modes recover 80% of the energy of the near-field pressure fluctuations. The method is subsequently applied to analyse the unsteady vortex shedding behind a cylinder. It shows that similar modes to POD can be robustly identified, but their strengths depend crucially on the observable and its locations, suggesting that these modes correlate differently with observables at different locations. This diagnosis would be useful for determining the optimal location for placing the feedback sensor in a closed-loop control of the vortex shedding behind a cylinder. + +Note that the examples shown in the paper are only for illustrations. They suffice for the purpose of demonstrating the potential uses of CCD, but further improvements are needed for a more in-depth analysis. For example, we can see that both the sampling frequency and sampling duration in the jet example are rather limited; therefore, a faster sampling of the observable, together with a longer sampling duration, is needed for more accurate diagnosis. In addition, a possible far-field noise diagnosis using pressure or the Lighthill stress tensors as the flow variables may be conducted. These, together with the application of CCD in the spectral space such as that shown in section 2.1, form some of our future work. + +# Acknowledgements + +The author wishes to thank Prof. Ann Dowling and Prof. Tim Colonius for the stimulating discussions on including multiple observables and the low-rank properties of CCD modes. The author would like to gratefully thank Prof. Jie Yao for sharing the DNS data of channel flows. The author is very grateful to Dr. Cong Wang and his collaborators for agreeing to use their PIV data on cylinder wakes. The author wishes to thank Dr. I. Naqavi who performed the LES simulations in our last collaborative publication (Lyu et al. 2017), from which part of the data is extracted and reused. The author also wishes to gratefully acknowledge the National Natural Science Foundation of China (NSFC) under the grant number 12472263. + +Declaration of interests. The authors report no conflict of interest. + +# Appendix A. The effects of sampling frequency, duration and multiple observables + +In this appendix, we aim to demonstrate the effects of sampling frequency, duration and multiple observables using the one-dimensional synthetic example introduced in section 3.1. We first demonstrate the effects of varying the sampling frequency of the observable $f_{s}^{p}$ . In figure 3, a large sampling frequency of $128 / 2\pi$ is used. This is a sufficiently large number considering one only needs to resolve the approximately maximum frequency of $6/2\pi$ required by the fifth term in (3.1). According to Nyquist's theorem, a minimum sampling frequency of $12/2\pi$ is needed. To demonstrate the validity of this requirement, we first perform CCD using $f_{s}^{p} = 12/2\pi$ , resulting in extracted modes virtually the same as those in figure 3. Subsequently, we use an under-resolved sampling frequency of $10/2\pi$ while are other parameters remain unchanged. The resulting modes are shown in figure 18. Clearly, although the low-frequency modes shown in figure 18(a) and (b) can still be correctly captured, the high-frequency modes shown in figure 18(c) and (d) start to differ from their correct forms. This is because the under-sampling causes aliasing effects in the decomposition, leading to incorrect modes 4 and 5. + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/8caa0e061600aba4bfc66fef0b215ecb9cd5e035b8b68936d44066475f7a17d6.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/29af093c51a7cb9ae90c3d7d1a632a023f0277ea93bc2089593633b6ad228e5f.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/8d956b65ab4cd42412075bf0cca3e20aed43639cb59e41fb4a9c5cea2dfe8e92.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/5222f11258621410683f089893b4205cd6273a29d957276156075a37beb08546.jpg) + + + +Figure 18: Extracted CCD modes when the observable is sampled using an under-resolved frequency $f_{s}^{p}=10/2\pi$ while $N=10^{4}$ , Q=128 and $f_{s}^{u}=128/2\pi$ . With this low sampling rate, modes 4 and 5 cannot be captured accurately. + + +We then demonstrate the effects of varying the sampling duration of the observable. In figure 3, Q is taken as 128 as this is the minimum value to use in order to resolve the flow structures given in (3.1), resulting in a frequency resolution of $f_{s}/Q = 1/2\pi$ . If a smaller Q such as 64 is used, one would expect the failure of resolving the low-frequency structures. This is precisely the case, as shown in figure 19, where Q = 64 while all other parameters remain the same. As shown in figure 19(a), the mode at the lowest frequency of $1/2\pi$ is extracted incorrectly. In addition, the next mode at the frequency of $1/\pi$ seems resolved incorrectly as well (see figure 19(b)). In fact, the modes shown in figure 19(a) and (b) appear to have somehow mixed. This is expected, since the frequency resolution is only $1/\pi$ , but the two modes differ from each other only by $1/2\pi$ . On the other hand, modes shown in figure 19(c) and (d) are characterised by frequencies of $3/\pi$ and $2/\pi$ respectively, and therefore have been resolved correctly. Figure 19 clearly shows that the number of shifted rows Q determines the frequency resolution in a similar manner to that in DFT. + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/b542af658dcb7e3fbfb053b05b57cc726ddd96a330af4f418020a4f450ec1a7a.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/231fd69522c6ea1fa65d7ae079cccc6e77630c3de88daae4f89209cbc84811da.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/85990d650d16f62ec1f11045c6f18a9c2c159b557dff447031c662cb2e787a09.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/21762de206ccaf2b373f53d78c12e6f23f77a7dac6067dc9deb0ad24e583594b.jpg) + + + +Figure 19: Extracted CCD modes when the observable is sampled for a short duration, i.e. Q = 64, while $N = 10^{4}$ and $f_{s}^{p} = f_{s}^{u} = 128/2\pi$ . With this short sampling duration, modes 1 and 2 cannot be resolved correctly. + + +We are now in a position to illustrate the effects of sampling frequency and duration of the flow u. Figure 3 shows that the extracted modes are subject to small random noise. This noise decreases rapidly as N increases. As mentioned in section 2.3, this is because the sampling rate and duration of the flow are to ensure the convergence of the correlation tensor (they do not affect the frequency limit and resolution). In figures 2 and 3, N is taken to be 10000. This is a large number because we deliberately chose an SNR that is as low as $10^{-4}$ . A small N can also be used at the expense of augmented noise in the resolved modes. For example, figure 20 shows the extracted CCD modes when N = 1000 (a-b) and N = 100 (c-d), respectively. Only modes 1, 2 and 4 are shown for brevity. Clearly, we see that using a duration of N = 1000 results in CCD modes that converge less well but are still unambiguously identified. When N reduces to 100, the resolved CCD modes are further corrupted by the random noise, nevertheless, the structures of the modes can still be recognized. Note that N here denotes the number of cycles of the sampled flow. At the lowest frequency of 100 Hz widely used in the fluid mechanics literature, N = 100 yields a sampling duration of 1 s. In the experiments, a record of 100 s can be easily managed. + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/2b63b306e6f06875a6f646c5b9f2df292aba101e31f07d7b414f8a0f96454439.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/3a1b364cfb2f8137241ba3187610264e9f3a3c7d90403567919081740506b26c.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/b7bec8203f82af7afc047aaaf00c0621b96d2a666cc518a3b18cf753a55a0b11.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/87919f87582ba19f15ba4cb43d057b185d0673b5d0f3e1960d31179c9e0d65ba.jpg) + + + +Figure 20: The extracted CCD modes when N = 1000 (a-b) and N = 100 (c-d) while other parameters remain the same as those in figure 3. Convergence increasingly deteriorates as N decreases. + + +Figure 20 shows that the CCD modes are corrupted significantly by random noise at an SNR $< 10^{-4}$ when $N = 100$ . However, when the random noise is only two orders of magnitude more energetic, $N = 100$ yields sufficiently well-resolved CCD modes. In general, we find that to obtain the same level of convergence, $N$ scales roughly as 1/SNR. Conversely, if longer samples are readily available, CCD can robustly extract the observable-correlated flow events at the same level of convergence at an even lower SNR. Therefore, CCD is especially suitable for analysing data acquired in experiments, where the data may be recorded for as long as desired. + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/ff840f5d3e2bb2d3c8effb1047afa59ad6eb1f74606f1d6d935828cf74750804.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/08b39d3e2e764d3e4e8c3fd9caa05e394c22a5b863b70c947269fb8ac42e31ed.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/d542ba6ff43a49f4d8358f0c61266c713fd2557114f0a23456f26b0db34b1dab.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/b6a310d176adbf9e7e038abe006423d0d710f1ef243f4733d8c626caaf2a16e9.jpg) + + + +Figure 21: The extracted CCD modes using a low flow sampling frequency of $f_{s}^{u} = 16/2\pi$ while $N = 8 \times 10^{4}$ , Q = 128, $f_{s}^{p} = 128/2\pi$ . Negligible change occurs compared to figure 3 because the same number of flow snapshots are used for the temporal average. + + +It is worth noting that although the extracted modes in figure 20 are subject to stronger noise contamination due to the insufficient convergence level, they do not suffer from aliasing effects due to under sampling. The fact that the sampling frequency and duration of the flow do not affect the frequency limit and resolution can be even more clearly demonstrated in figure 21. Figure 21 shows the extracted modes when the flow is only sampled at $16 / 2\pi$ . However, to exclude the effects of insufficient convergence, the total number of flow snapshots is kept the same. Comparing figures 3 and 21 reveals that the extracted modes are virtually identical, demonstrating the independence of the frequency resolution on the temporal duration and frequency of sampling. + +We are now in a position to demonstrate the effects of including multiple observables. The flow field u takes the same form of $(3.1)$ . However, more observables need to be defined. To ensure that the observables are not only similar to $(3.2)$ but also exhibit variations, we construct up to ten observables $p_{i}(t)$ ( $i = 1, 2, 3 \ldots 10$ ) such that + +$$ +\begin{array}{l} p _ {i} (t) = \left(1 + 0. 2 r _ {1 i}\right) \cos \left(t - \frac {\pi}{4}\right) + \left(1 + 0. 2 r _ {2 i}\right) \sin \left(2 t - \frac {\pi}{3}\right) + \left(1 + 0. 2 r _ {3 i})\right) \cos (4 t) \tag {A1} \\ + (1 + 0. 2 r _ {4 i}) \cos (6 t - \frac {\pi}{1 2}) + 1 0 0 r _ {i} (t), \\ \end{array} +$$ + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/40244f4a1338849948b2a9e818a3f6883f8b412db8f10921fc1ef33c754ae8f1.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/424e568b81c5f4645fa4445be64905ffd44aa417a66d7d119493976b7e75698d.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/48f6568c702256ae79fe87eb84810a68c1fe7a054bab7925234e0258ff1d64f3.jpg) + + +![image](https://cdn-mineru.openxlab.org.cn/result/2026-06-07/b70089ee-fa1a-4e57-9e24-8f0805a1690f/b95bfd3f8d3670f7bbf72c281e1ad202ff0e9891921273c1272eb0fa3ff03ba4.jpg) + + + +Figure 22: The extracted CCD modes 1 and 4 when 1 (a-b) and 10 (c-d) observables are included. Including multiple observables improves the convergences of the resulting CCD modes. + + +where $r_{ji}$ (j = 1, 2, 3, 4) are random numbers between [-0.5, 0.5] and $r_{i}(t)$ a random function with a uniform distribution over [-0.5, 0.5]. Note that a very strong random noise (SNR $\sim 10^{-4}$ ) is also added in each observable in order to demonstrate the validity of the decomposition with strongly contaminated observables. + +Following the procedure listed in section 2.4, multiple observables can be included straightforwardly to form the matrix P. When identical parameters to those in figure 3 are used, the extracted modes using either 1 or 10 observables are shown in figures 22(a,b) and 22(c,d), respectively. Only modes 1, 2 and 4 are shown for brevity. Clearly, when the observables are also strongly contaminated, the extracted modes converge less satisfactorily, as shown in figure 22(a-b). However, by including 10 observables, the quality of the extracted modes improves significantly. Figure 22 shows that including more observables can indeed improve the convergence of the resulting CCD modes, particularly when the observables are corrupted by noise. + + + +N. Aubry. On the hidden beauty of the proper orthogonal decomposition. Theoretical and Computational Fluid Dynamics, 2:339-352, 1991. + + + + + +G. Berkooz, P. Holmes, and J. L. Lumley. The proper orthogonal decomposition in the analysis of turbulent flow. Annual Review of Fluid Mechanics, 25:539–575, 1993. + + + + + +J. Borée. Extended proper orthogonal decomposition: a tool to analyse correlated events in turbulent flows. Experiments in Fluids, 35:188–192, 2003. + + + + + +B. Bugeat, U. Karban, A. Agarwal, L. Lesshafft, and P. Jordan. Acoustic resolvent analysis of turbulent jets. Theoretical and Computational Fluid Dynamics, 38:687–706, 2024. + + + + + +H. Choi, W. Jeon, and J. Kim. Control of flow over a bluff body. Annual Review of Fluid Mechanics, 40:113–139, 2008. + + + + + +M. J. Colbrook, L. J. Ayton, and M. Szöke. Residual dynamic mode decomposition: robust and verified Koopmanism. Journal of Fluid Mechanics, 955(A21), 2023. + + + + + +S. C. Crow and F. H. Champagne. Orderly structure in jet turbulence. Journal of Fluid Mechanics, 48:547–591, 1971. + + + + + +B. Farrell and J. Ioannou. Stochastic forcing of the linearized Navvier-Stokes equations. Physics of Fluids, 5(11):2600-2609, 1993. + + + + + +J. B. Freund and T. Colonius. Turbulence and sound-field POD analysis of a turbulent jet. International Journal of Aeroacoustics, 8(4):337–354, 2009. + + + + + +Mohamed Gad-el Hak. Flow control: passive, active, and reactive flow management. Cambridge University Press, 2007. + + + + + +H. Hotelling. Relations between two sets of variates. Biometrika, 28:321-377, 1936. + + + + + +J. Jeun, J. W. Nichols, and M. R. Jovanović. Input-output analysis of high-speed axisymmetric isothermal jet noise. Physics of Fluids, 28(4):047101, 2016. + + + + + +M. R. Jovanović. From bypass transition to flow control and data-driven turbulence modeling: an input-output viewpoint. Annual Review of Mechanics, 53:311–45, 2021. + + + + + +John Kim. Physics and control of wall turbulence for drag reduction. Philosophical Transactions of the Royal Society of London A: Mathematical, Physical and Engineering Sciences, 369 (1940):1396–1411, 2011. + + + + + +B. O. Koopman. Hamiltonian systems and transformations in Hilbert space. PNAS, 17:315-318, 1931. + + + + + +M. Lee and R. Moser. Direct numerical simulation of turbulent channel flow up to Re 5200. Journal of Fluid Mechanics, 774:395–415, 2015. + + + + + +J. L. Lumley. The structure of inhomogeneous turbulence. Atmospheric Turbulence and Wave Propagation, pages 166–178, 1967. ed. AM Yaglom, VI Tatarsky, Nauka, Moscow. + + + + + +J. L. Lumley. Stochastic tools in turbulence. Academic Press, 1970. + + + + + +B. Lyu and A. Dowling. Modelling installed jet noise due to the scattering of jet instability waves by swept wings. Journal of Fluid Mechanics, 870:760–783, 2019. + + + + + +B. Lyu, A. Dowling, and I. Naqavi. Prediction of installed jet noise. Journal of Fluid Mechanics, 811:234–268, 2017. + + + + + +S. Maurel, J. Borée, and J. L. Lumley. Extended proper orthogonal decomposition: application to jet/vortex interaction. Flow, turbulence and combustion, 67:125–136, 2001. + + + + + +B. J. Mckeon. A critical-layer framework for turbulent pipe flow. Journal of Fluid Mechanics, 658:336–382, 2010. + + + + + +I. Mezić. Analysis of fluid flows via spectral properties of the Koopman operator. Annual Review of Fluid Mechanics, 45:357–378, 2013. + + + + + +D. S. Park, D. M. Ladd, and E. W. Hendricks. Feedback control of von Karman vortex shedding behind a circular cylinder at low Reynolds numbers. Physics of Fluids, 6(2390), 1994. + + + + + +E. Pickering, A. Towne, P. Jordan, and T. Colonius. Resolvent-based modelling of turbulent jet noise. The Journal of the Acoustical Society of America, 150:2421–2433, 2021. + + + + + +P. I. Renn, C. Wang, S. Lale, Z. Li, A. Anandkumar, and M. Gharib. Forecasting subcritical cylinder wakes with Fourier Neural Operators. ArXiv, 2301:08290, 2023. + + + + + +F. Riesz and B. Nagy. Functional analysis. Ungar, New York, 1955. + + + + + +K. Roussopoulos. Feedback control of vortex shedding at low Reynolds numbers. Journal of Fluid Mechanics, 248:267–296, 1993. + + + + + +C. W. Rowley. Model reduction for fluids using balanced proper orthogonal decomposition. International Journal of Bifurcation and Chaos, 15(3):997-1013, 2005. + + + + + +P. Schmid. Dynamic mode decomposition of numerical and experimental data. Journal of Fluid Mechanics, 656:5-28, 2010. + + + + + +P. Schmid. Dynamic mode decomposition and its variants. Annual Review of Fluid Mechanics, 54:225-254, 2022. + + + + + +O. T. Schmidt and P. J. Schmid. A conditional space-time pod formalism for intermittent and rare events/ example of acoustic bursts in turbulent jets. Journal of Fluid Mechanics, 867:R2, 2019. + + + + + +O. T. Schmidt, A. Towne, G. Rigas, T. Colonius, and G. A. Brés. Spectral analysis of jet turbulence. Journal of Fluid Mechanics, 855:953–982, 2018. + + + + + +A. S. Sharma and B. J. Mckeon. On coherent structure in wall turbulence. Journal of Fluid Mechanics, 728:196–238, 2013. + + + + + +A. Sinha, D. Rodriguez, A. B. Guillaume, and T. Colonius. Wavepacket models for supersonic jet noise. Journal of Fluid Mechanics, 742:71–95, 2014. + + + + + +L. Sirovich. Turbulence and the dynamics of coherent structures, Parts I-III. Quarterly of Applied Mathematics, 45(3):561–590, 1987. + + + + + +L. F.de Souza, R. F. Miotto, and W. R. Wolf. Analysis of transient and intermittent flows using a multidimensional empirical mode decomposition. Theoretical and Computational Fluid Dynamics, 38:291-311, 2024. + + + + + +K. Taira, S. Brunton, S. T. M. Dawson, C. W. Rowley, T. Colonius, B. J. Mckeon, O. T. Schmidt, S. Gordeyev, V. Theofilis, and L. S. Ukeiley. Modal analysis of fluid flows: an overview. AIAA Journal, 55:4013–4041, 2017. + + + + + +V. Theofilis. Global linear instability. Annual Review of Fluid Mechanics, 43(1):319–352, 2011. + + + + + +A. Towne, O. T. Schmidt, and T. Colonius. Spectral proper orthogonal decomposition and its relationship to dynamic mode decomposition and resolvent analysis. Journal of Fluid Mechanics, 847:821–867, 2018. + + + + + +L. N. Trefethen, A. E. Trefethen, S. C. Redyy, and T. A. Driscoll. Hydrodynamic stability without eigenvalues. Science, 261(5121):578–584, 1993. + + + + + +J. E. Williams and B. C. Zhao. The active control of vortex shedding. Journal of Fluids and Structures, 3:115–122, 1989. + + + + + +M. O. Williams, I. G. Kevrekidis, and C. W. Rowley. A data-driven approximation of the Koopman operator: extending dynamic mode decomposition. Journal of Nonlinear Science, 25:1307–1346, 2015. + diff --git a/src/CCD_analysis/README.md b/src/CCD_analysis/README.md index cc02366..7b6ce0e 100644 --- a/src/CCD_analysis/README.md +++ b/src/CCD_analysis/README.md @@ -1,30 +1,85 @@ -# CCD_analysis +# CCD_analysis: Correction-Field CCD Pipeline -Canonical Correlation Decomposition for fluidic pinball control analysis. +Analyzes DRL-controlled fluidic pinball using **correction-field decomposition** + **Canonical Correlation Decomposition (CCD/Lyu23)**. Core question: does `dq_ctl` (what the controller adds) match `dq_tar` (what the target requires)? ## Quick Start -1. Read `ccd_knowledge.md` — 唯一知识库,包含理论、流程、结果 -2. Run comparison: `conda run -n pycuda_3_10 python3 correction_analysis/compare_dqctl_scenes.py` -3. Run diagnostics: `conda run -n pycuda_3_10 python3 correction_analysis/diagnose_corrections.py` +```bash +# 1. Panorama comparison figure (primary output) +conda run -n pycuda_3_10 python3 correction_analysis/compare_dqctl_scenes.py -## Directory +# 2. CCD quantitative decomposition +conda run -n pycuda_3_10 python3 correction_analysis/decompose_corrections.py -``` -correction_analysis/ — 所有当前分析代码 -scripts/ — GPU 数据采集 + phase alignment -utils/ — 核心算法(POD/CCD/场加载/平移) -ccd/ — Round 5 旧基线(已冻结) -data/ - figures/ — 诊断图(无 colorbar) - ccd/ — JSON 结果 - old_data/ — 归档废弃数据 +# 3. Single-scene diagnostics (zone metrics) +conda run -n pycuda_3_10 python3 correction_analysis/diagnose_corrections.py ``` -## Environment +## Pipeline Architecture ``` -conda run -n pycuda_3_10 +scripts/collect_*.py → scripts/{detect_period,replay_fields}.py + (GPU采集) (phase alignment) + ↓ +correction_analysis/compute_correction_fields.py + (dq_blk, dq_ctl, dq_tar) + ↓ + ┌────┴────┬────────────┐ + ↓ ↓ ↓ +compare_ decompose_ diagnose_ +dqctl_ corrections corrections +scenes.py .py .py +(全景对比) (CCD定量) (zone诊断) ``` -详见 `ccd_knowledge.md`。 +## Directory Structure + +``` +CCD_analysis/ + README.md # 本文件 + ccd_knowledge.md # 唯一知识库 (理论, 结果, 操作流程, bug经验) + Lyu23.md # CCD 方法文献 + configs.py # 场景元数据 (统一几何) + utils/ + resampling.py # POD, CCD, 场加载 + cfd_interface.py # LegacyCelerisLab 封装 (GPU) + build_observation + field_translate.py # 场平移 (备用) + load_vortex_fields.py # 瞬态 vortex 场加载 + scripts/ + collect_*.py # GPU 数据采集 + detect_period.py # 周期检测 → phase_plan.json + replay_fields.py # 场回放 → fields_aligned.npz + correction_analysis/ + compute_correction_fields.py # dq 计算 (核心) + compare_dqctl_scenes.py # 全景对比图 (核心输出) + decompose_corrections.py # CCD 定量 (POD + force/action CCD) + diagnose_corrections.py # 单场景 zone 诊断 + run_signature_line.py # Signature CCD + run_zone_ccd.py # Zone-restricted CCD + run_steady_metrics.py # Steady cloak 定量 + run_15L_correction.py # 1.5L 专项 + visualize_action_ccd.py # Action-CCD mode1 可视化 + process_legacy_steady.py # 旧格式加载 + ccd/ # Round 5 冻结基线 (勿改) + data/ + figures/ # 诊断图 (仅保留核心对比图) + ccd/ # JSON 结果 + old_data/ # 归档 (废弃脚本/旧报告/旧数据) +``` + +## Key Documentation + +| File | Content | +|------|---------| +| `ccd_knowledge.md` | **Primary entry** — theory, results, conventions, bug history | +| `Lyu23.md` | CCD method paper (Lyu 2023) | +| `data/old_data/ccd_correction_field_report.md` | Old full report (archived reference) | +| `data/old_data/ccd_handover.md` | Old handover notes (archived) | + +## Key Conventions + +- **Main analysis object**: `dq_ctl` (not raw `q_ctl`) +- **Observation order**: `[forces/force_norm, sensors/sens_norm]` — force first (see Bug 3 in ccd_knowledge.md §12) +- **All scenes unified geometry**: pinball center at 613px, sensors at 800px +- **Environment**: `conda run -n pycuda_3_10` +- **GPU**: Device 2 for collection diff --git a/src/CCD_analysis/ccd/validate.py b/src/CCD_analysis/ccd/validate.py new file mode 100644 index 0000000..ea6c661 --- /dev/null +++ b/src/CCD_analysis/ccd/validate.py @@ -0,0 +1,270 @@ +"""LOCO and blocked-split validation for CCD (Round 5). + +Reuses shared data loader from resampling.py. +Target-only POD basis. Q_delay=6 for force/action. + +0.75L and 1.0L only. 1.5L excluded from validation. + +Usage: + conda run -n pycuda_3_10 python ccd/validate.py +""" +from __future__ import annotations + +import json +import os +import sys +import time + +import numpy as np + +_SRC = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +if _SRC not in sys.path: + sys.path.insert(0, _SRC) + +from CCD_analysis.configs import DATA_DIR +from CCD_analysis.utils.resampling import ( + compute_reduced_ccd, cumulative_energy, + load_aligned_fields, make_force_obs, + build_field_matrix, project_into_basis, +) + +R_LIST = [6, 8, 10] +CCD_Q = 6 +N_CYCLES = 4 +N_PTS = 24 +DIAMETERS = [0.75, 1.0] + + +def r2(y_true: np.ndarray, y_pred: np.ndarray) -> float: + """Coefficient of determination.""" + ss_r = np.sum((y_true - y_pred) ** 2) + ss_t = np.sum((y_true - np.mean(y_true)) ** 2) + return float(1.0 - ss_r / (ss_t + 1e-12)) + + +def reconstruct_observable(W, sigma, R, a_test, y_train): + """Reconstruct observable from CCD modes. + + Returns dict with 'mode1' and 'm80' reconstructions. + """ + am = np.mean(a_test, axis=1, keepdims=True) + as_ = np.std(a_test, axis=1, keepdims=True) + 1e-12 + a_test_z = (a_test - am) / as_ + z_test = W.T @ a_test_z + + ym = np.mean(y_train, axis=1, keepdims=True) + ys = np.std(y_train, axis=1, keepdims=True) + 1e-12 + half = CCD_Q // 2 + m_obs = y_train.shape[0] + + en = cumulative_energy(sigma) + m80 = int(np.searchsorted(en, 0.80) + 1) if len(en) > 0 else 1 + + results = {} + + # Mode-1 + if R.shape[1] >= 1: + pz_1 = R[:, :1] * sigma[:1] @ z_test[:1, :] + yp_1 = pz_1[half * m_obs:(half + 1) * m_obs, :] * ys + ym + results["mode1"] = yp_1 + else: + results["mode1"] = np.zeros_like(y_train[:, :a_test.shape[1]]) + + # M80 + n_rm = min(m80, R.shape[1]) + if n_rm >= 1: + pz_m = R[:, :n_rm] * sigma[:n_rm] @ z_test[:n_rm, :] + yp_m = pz_m[half * m_obs:(half + 1) * m_obs, :] * ys + ym + results["m80"] = yp_m + else: + results["m80"] = np.zeros_like(y_train[:, :a_test.shape[1]]) + + return results + + +def run_single_diameter(diam: float, scene_data: dict) -> dict: + """Run validation for one diameter. Returns results dict.""" + tgt_key = f"target_cylinder_{diam}L" + ill_key = f"illusion_{diam}L" + + tgt_d = scene_data[tgt_key] + ill_d = scene_data[ill_key] + unc_d = scene_data["pinball"] + + # Pre-build target-only field matrices + tgt_f = build_field_matrix(tgt_d["ux"], tgt_d["uy"]) + ill_f = build_field_matrix(ill_d["ux"], ill_d["uy"]) + unc_f = build_field_matrix(unc_d["ux"], unc_d["uy"]) + + diam_results = {} + pod_cache = {} + + # Pre-compute target-only POD for each fold + for fold in range(N_CYCLES): + test_cyc = fold + train_cyc = [c for c in range(N_CYCLES) if c != test_cyc] + train_idx = sorted([c * N_PTS + p for c in train_cyc for p in range(N_PTS)]) + for r in R_LIST: + Q_ref = tgt_f[:, train_idx] + mf = np.mean(Q_ref, axis=1) + U, _, _ = np.linalg.svd(Q_ref - mf[:, None], full_matrices=False) + pod_cache[("loco", fold, r)] = (mf, U[:, :r]) + + # Blocked split + train_idx_full = list(range(0, 2 * N_PTS)) + for r in R_LIST: + Q_ref = tgt_f[:, train_idx_full] + mf = np.mean(Q_ref, axis=1) + U, _, _ = np.linalg.svd(Q_ref - mf[:, None], full_matrices=False) + pod_cache[("blocked", 0, r)] = (mf, U[:, :r]) + + # -- LOCO -- + print("\n--- LOCO (4-fold) ---", flush=True) + loco_results = {} + for r in R_LIST: + for obs in ["force_fy", "force_fx", "action"]: + fold_r2_m1, fold_r2_m80 = [], [] + for fold in range(N_CYCLES): + test_cyc = fold + train_cyc = [c for c in range(N_CYCLES) if c != test_cyc] + train_idx = sorted([c * N_PTS + p for c in train_cyc for p in range(N_PTS)]) + test_idx = sorted([c * N_PTS + p for c in [test_cyc] for p in range(N_PTS)]) + + mf, modes_r = pod_cache[("loco", fold, r)] + + for name, d, fld in [ + (tgt_key, tgt_d, tgt_f), (ill_key, ill_d, ill_f), ("pinball", unc_d, unc_f) + ]: + if obs == "action" and "illusion" not in name: + continue + if d.get("forces") is None and "force" in obs: + continue + + a_train = modes_r.T @ (fld[:, train_idx] - mf[:, None]) + a_test = modes_r.T @ (fld[:, test_idx] - mf[:, None]) + + if "force" in obs: + f_mode = obs.split("_")[1] # "fy" or "fx" + y_train = make_force_obs(d["forces"][train_idx], name, mode=f_mode) + y_test = make_force_obs(d["forces"][test_idx], name, mode=f_mode) + else: + y_train = d["actions"][train_idx, :].T + y_test = d["actions"][test_idx, :].T + + W, sigma, Rmat, _, _, _ = compute_reduced_ccd(a_train, y_train, Q_delay=CCD_Q) + recon = reconstruct_observable(W, sigma, Rmat, a_test, y_train) + + ch_m1 = [r2(y_test[c], recon["mode1"][c]) for c in range(y_test.shape[0])] + ch_m80 = [r2(y_test[c], recon["m80"][c]) for c in range(y_test.shape[0])] + fold_r2_m1.append(float(np.mean(ch_m1))) + fold_r2_m80.append(float(np.mean(ch_m80))) + + if fold_r2_m1: + key = f"LOCO_{obs}_r{r}" + loco_results[key] = { + "mode1": { + "mean": float(np.mean(fold_r2_m1)), + "std": float(np.std(fold_r2_m1)), + }, + "m80": { + "mean": float(np.mean(fold_r2_m80)), + "std": float(np.std(fold_r2_m80)), + }, + } + print(f" {key}: R2_m1={loco_results[key]['mode1']['mean']:.4f}+-" + f"{loco_results[key]['mode1']['std']:.4f} " + f"R2_m80={loco_results[key]['m80']['mean']:.4f}+-" + f"{loco_results[key]['m80']['std']:.4f}", flush=True) + + # -- Blocked split -- + print("\n--- Blocked Split (train=0-47, test=48-95) ---", flush=True) + blocked = {} + test_idx_full = list(range(2 * N_PTS, 4 * N_PTS)) + for r in R_LIST: + for obs in ["force_fy", "force_fx", "action"]: + mf, modes_r = pod_cache[("blocked", 0, r)] + per_case_m1, per_case_m80 = {}, {} + + for name, d, fld in [ + (tgt_key, tgt_d, tgt_f), (ill_key, ill_d, ill_f), ("pinball", unc_d, unc_f) + ]: + if obs == "action" and "illusion" not in name: + continue + if d.get("forces") is None and "force" in obs: + continue + + a_train = modes_r.T @ (fld[:, train_idx_full] - mf[:, None]) + a_test = modes_r.T @ (fld[:, test_idx_full] - mf[:, None]) + + if "force" in obs: + f_mode = obs.split("_")[1] + y_train = make_force_obs(d["forces"][train_idx_full], name, mode=f_mode) + y_test = make_force_obs(d["forces"][test_idx_full], name, mode=f_mode) + else: + y_train = d["actions"][train_idx_full, :].T + y_test = d["actions"][test_idx_full, :].T + + W, sigma, Rmat, _, _, _ = compute_reduced_ccd(a_train, y_train, Q_delay=CCD_Q) + recon = reconstruct_observable(W, sigma, Rmat, a_test, y_train) + + ch_m1 = [r2(y_test[c], recon["mode1"][c]) for c in range(y_test.shape[0])] + ch_m80 = [r2(y_test[c], recon["m80"][c]) for c in range(y_test.shape[0])] + per_case_m1[name] = float(np.mean(ch_m1)) + per_case_m80[name] = float(np.mean(ch_m80)) + + key = f"blocked_{obs}_r{r}" + blocked[key] = { + "mode1": { + "mean": float(np.mean(list(per_case_m1.values()))), + "per_case": per_case_m1, + }, + "m80": { + "mean": float(np.mean(list(per_case_m80.values()))), + "per_case": per_case_m80, + }, + } + print(f" {key}: R2_m1={blocked[key]['mode1']['mean']:.4f} " + f"R2_m80={blocked[key]['m80']['mean']:.4f}", flush=True) + + diam_results["LOCO"] = loco_results + diam_results["blocked_split"] = blocked + return diam_results + + +def run(): + print("=" * 60, flush=True) + print("CCD Validation (Round 5)", flush=True) + print("=" * 60, flush=True) + + t_start = time.time() + all_results = {} + + for diam in DIAMETERS: + tgt_key = f"target_cylinder_{diam}L" + ill_key = f"illusion_{diam}L" + print(f"\n{'=' * 60}", flush=True) + print(f"Diameter {diam}L", flush=True) + print(f"{'=' * 60}", flush=True) + + t0 = time.time() + scene_data = { + tgt_key: load_aligned_fields(tgt_key), + ill_key: load_aligned_fields(ill_key), + "pinball": load_aligned_fields("pinball"), + } + print(f" Data loaded in {time.time() - t0:.0f}s", flush=True) + + t1 = time.time() + all_results[f"{diam}L"] = run_single_diameter(diam, scene_data) + print(f" Analysis done in {time.time() - t1:.0f}s", flush=True) + + out_dir = os.path.join(DATA_DIR, "ccd") + with open(os.path.join(out_dir, "validation_results.json"), "w") as f: + json.dump(all_results, f, indent=2) + + print(f"\nTotal: {time.time() - t_start:.0f}s", flush=True) + print(f"Saved to {out_dir}/validation_results.json", flush=True) + + +if __name__ == "__main__": + run() diff --git a/src/CCD_analysis/ccd_knowledge.md b/src/CCD_analysis/ccd_knowledge.md index e4f487d..aafbaf1 100644 --- a/src/CCD_analysis/ccd_knowledge.md +++ b/src/CCD_analysis/ccd_knowledge.md @@ -241,17 +241,24 @@ def compute_reduced_ccd(pod_coeffs, observable, Q_delay=6): ## 5. 结果 -### 5.1 Correction-field CCD 主表 +### 5.1 Correction-field CCD 主表(2026-06-28 更新,统一几何后重跑) | 指标 | 0.75L | 1.0L | 1.5L | |------|-------|------|------| -| **O(dqctl, dqtar) mode1** | **0.564** | **0.913** | **0.667** | -| force_fy m80 | 2 | **1** (r=8/10) | 2 | -| action sigma1 | 1.39 | 1.13 | **0.28** | -| O(force, sig) tau=0 | **0.413** | **0.551** | — | -| O(force, sig) tau=tau_c | **0.806** | **0.768** | — | +| **O(dqctl, dqtar) mode1 (r=6)** | **0.383** | **0.926** | **0.922** | +| **O(dqctl, dqtar) mode1 (r=10)** | **0.320** | **0.684** | **0.661** | +| force_fy m80 (r=6) | 2 | 2 | **1** | +| action sigma1 (r=6) | 1.49 | 1.17 | **0.20** | | Phase drift | low | low | **high** | -| Body-wake/sensor KE ratio | 0.73 | 1.17 | **2.58** | +| 1.5L special: rank sensitivity | — | — | O drops 0.922→0.661 (r=6→10) | + +**关键变化**(vs 6月15日旧版,Illusion 旧几何 pinball x≈393px): +- 0.75L O 从 0.564 → **0.383**(-32%):旧几何的空间错位虚高了 overlap。统一几何后揭示真实匹配度远低于预期。 +- 1.0L O 从 0.913 → **0.926**(+1.4%):基本不变,1.0L 的控制修正与目标一致。 +- 1.5L O(r=6)=**0.922**(首次获得):dominant mode 匹配度高,但更高阶 mode 快速发散(r=10 时降至 0.661),反映多尺度控制策略。 +- 1.5L action sigma1=**0.20**(远低于 0.75L 的 1.49 和 1.0L 的 1.17):确认高频调制机制下动作与流场结构的映射极其分散。 + +以下 force-sig overlap 和 zone 数据来自 6 月 15 日旧版(待用统一几何和新 zone 定义重跑): ### 5.2 三区域 Force-Signature Overlap @@ -284,8 +291,8 @@ def compute_reduced_ccd(pod_coeffs, observable, Q_delay=6): |------|------------------|------| | steady_cloak | 0.196 | 稳态,开环 | | karman_re100 | 0.397 | 周期,PPO 闭环 | -| vortex_lamb | 0.157 | 瞬态,PPO 闭环 | -| vortex_taylor | 0.191 | 瞬态,PPO 闭环 | +| vortex_lamb | 0.164 | 瞬态,PPO 闭环(fade-in/out + swapped norm) | +| vortex_taylor | 0.203 | 瞬态,PPO 闭环(fade-in/out + swapped norm) | **结论**:不论上游条件如何(稳态/周期涡街/瞬态涡),控制策略的基本物理机制一致——"通过后两圆柱旋转补偿 pinball 阻塞引起的速度亏损,前端圆柱调节升力"。这与 SR 分析的结论完全吻合。 @@ -299,7 +306,7 @@ def compute_reduced_ccd(pod_coeffs, observable, Q_delay=6): **结论**:开环恒速旋转几乎无法抑制波动。需要闭环 DRL 控制。 -### 5.5 Action-CCD Mode 1 可视化 +### 5.5 Action-CCD Mode 1 Action-CCD 找出了控制器直接调制的主要结构。对 Cloak 场景,action-CCD mode 1 ≈ dq_ctl mean field,确认了"控制调制的结构 = correction-field 的主成分"的直觉。 @@ -311,14 +318,13 @@ Action-CCD 找出了控制器直接调制的主要结构。对 Cloak 场景,ac 所有位于 `data/figures/` 下(无 colorbar,干净布局,裁剪到 x=300-1100): -| 图 | 内容 | 列数 | -|----|------|------| -| `corr_comparison_all_scenes.png` | 7 场景全景(4 cloak + 3 illusion) | 7 × 4 行 | -| `corr_illusion_comparison_dqctl.png` | Illusion 三直径对比 | 3 × 4 行 | -| `corr_cloak_comparison_dqctl.png` | Cloak 四场景对比 | 4 × 4 行 | -| `corr_illusion_{diam}L_dq_ctl/blk/tar_*.png` | 各场景单独 dq 图 | 3/1 panels | -| `corr_illusion_{diam}L_ctl_vs_tar.png` | dq_ctl vs dq_tar 对比 | 2×2 | -| `action_ccd_mode1_{scene}.png` | Action-CCD mode1 | 1×3 | +| 图 | 内容 | +|----|------| +| `corr_comparison_all_scenes.png` | 7 场景全景(4 cloak + 3 illusion),4 行 × 7 列 | +| `corr_cloak_comparison_dqctl.png` | Cloak 四场景 dq_ctl 对比 | +| `corr_illusion_comparison_dqctl.png` | Illusion 三直径 dq_ctl 对比 | +| `corr_{scene}_ctl_vs_tar.png` | 单场景 dq_ctl vs dq_tar 对比(2×2) | +| `steady_cloak_cancel_test.png` | Steady cloak 抵消检验 | ### 6.2 全景对比图的读法 @@ -389,33 +395,37 @@ conda run -n pycuda_3_10 python3 correction_analysis/compare_dqctl_scenes.py ``` src/CCD_analysis/ - ccd_knowledge.md <-- 本文档(唯一知识库) - configs.py -- 场景元数据 + ccd_knowledge.md ← 本文档(唯一知识库) + configs.py ← 场景元数据(统一几何) + README.md ← 快速入口 + Lyu23.md ← CCD 方法文献 + ccd/ ← Round 5 冻结基线(勿改) utils/ - resampling.py -- POD, CCD, 场加载 - field_translate.py -- 场平移对齐 - load_vortex_fields.py -- 瞬态 vortex 场加载 - cfd_interface.py -- LegacyCelerisLab 封装 (GPU) + resampling.py ← POD, CCD, 场加载 + field_translate.py ← 场平移(备用,不参与默认 pipeline) + load_vortex_fields.py ← 瞬态 vortex 场加载 + cfd_interface.py ← LegacyCelerisLab 封装 (GPU) scripts/ - detect_period.py -- 周期检测 → phase_plan.json - replay_fields.py -- 场回放 → fields_aligned.npz - collect_*.py -- GPU 数据采集 + detect_period.py ← 周期检测 → phase_plan.json + replay_fields.py ← 场回放 → fields_aligned.npz + collect_*.py ← GPU 数据采集 correction_analysis/ - compute_correction_fields.py -- correction-field 计算 - diagnose_corrections.py -- 诊断图生成 - compare_dqctl_scenes.py -- 多场景对比图 - decompose_corrections.py -- 旧 force/action CCD - run_signature_line.py -- 旧 signature CCD - run_15L_correction.py -- 旧 1.5L 分析 - run_zone_ccd.py -- 旧 zone CCD - run_steady_metrics.py -- 旧 steady cloak 度量 - process_legacy_steady.py -- 旧格式加载 + compute_correction_fields.py ← correction-field 计算 + diagnose_corrections.py ← 诊断图生成 + compare_dqctl_scenes.py ← 多场景对比图 + decompose_corrections.py ← CCD 定量分解(POD + force/action CCD) + run_signature_line.py ← signature CCD + run_15L_correction.py ← 1.5L 专项分析 + run_zone_ccd.py ← zone-restricted CCD + run_steady_metrics.py ← steady cloak 定量度量 + visualize_action_ccd.py ← action-CCD mode1 可视化 + process_legacy_steady.py ← 旧格式加载 data/ - {scene_id}/{scene_name}/ -- 各场景数据 - resampled/{scene}/ -- phase_plan.json - ccd/ -- JSON 结果文件 - figures/ -- PNG 图(无 colorbar 版本) - old_data/ -- 归档废弃数据 + {scene_id}/{scene_name}/ ← 各场景数据 + resampled/ ← phase_plan.json + ccd/ ← JSON 结果文件 + figures/ ← PNG 诊断图 + old_data/ ← 归档(旧报告、旧脚本、旧 resampled 数据) ``` --- @@ -449,8 +459,51 @@ src/CCD_analysis/ | 方向 | 状态 | 说明 | |------|------|------| | Karman cloak CCD 分析 | 数据已齐,分析延后 | 问题定义不同(distortion compensation) | -| 1.5L force-sig overlap | 待重跑 | SI=800 数据已有;SI=200 高频采集待做 | -| SR-CCD-OID 映射 | 草稿 | 归档在 `data/old_data/sr_ccd_oid_mapping.md`,需根据各方向最终报告校正 | -| 1.5L 高频采样 | 待做 | 子步采集(SI=200),解析高频控制结构 | -| 统一几何后 CCD 重跑 | 待做 | `decompose_corrections.py` 需加 1.5L 后重跑;`correction_ccd_results.json` 仍为 6 月 15 日旧版 | -| 项目目录清理 | 已完成 (2026-06-28) | 移除根级 `old_data/`、`old_scripts/`、`output/`、`steady/`;废弃脚本归档至 `data/old_data/`;文档更新至统一几何 | +| Vortex 数据采集 Bug 修复 | 已完成 | 修复 cylinder order swap(BOTTOM=id4, TOP=id5)+ FIFO warmup 导致涡量消失;重采集 Taylor/Lamb | +| SR-CCD-OID 映射 | 草稿 | 归档 `data/old_data/sr_ccd_oid_mapping.md`,需最终校正 | +| 统一几何后 CCD 重跑 | 已完成 | `correction_ccd_results.json` 已更新(含 1.5L) | +| Vortex 对比图更新 | 已完成 | `compare_dqctl_scenes.py` 已用修正数据重跑 | +| 项目目录清理 + 文档更新 | 已完成 | 移除废弃目录、SI=200 错误数据、旧诊断图 | + +--- + +## 12. Vortex 采集 Bug 排查经验(2026-06-29) + +`collect_vortex.py` 中发现了四个独立 bug,按发现顺序: + +**Bug 1 — 圆柱顺序对调** +- 训练 env `legacy_env_vortex.py` 添加顺序:front(id3) → TOP(+y, id4) → BOTTOM(-y, id5) +- action 映射:`temp[3]=front, temp[4]=TOP(bias=-4), temp[5]=BOTTOM(bias=+4)` +- 脚本先后把 TOP/BOTTOM 添加顺序写反 → 后两圆柱旋转方向全错 +- 症状:Lamb front 剧烈振荡 (std=0.28),dipole 对称性完全破坏 + +**Bug 2 — 涡量消失** +- FIFO warmup 在涡加入后跑 150×800=120000 步 → 涡从 x=15 漂出域外 (1280 lu) +- 症状:涡量场只有 pinball 尾流,完全看不到 vortex 结构 + +**Bug 3(核心)— 观测值归一化顺序错误** +- 训练 env 产出:`obs = [forces/force_norm, sensors/sens_norm]`(force 先) +- 脚本将 channel 和 norm 互换:`[sensors/force_norm, forces/sens_norm]` +- 模型收到不匹配分布的反馈 +- 症状:Lamb cross-corr 仅 0.73, Taylor 仅 0.50;后圆柱同向转而非反向 + +**Bug 4 — fade-in/out 缺失** +- `uni_test.ipynb` 有 25 步渐入 + 25 步渐出到 steady-cloak bias +- 脚本直接给全量 PPO action,无过渡 + +**最终修正方案**: +1. Cylinder order 匹配训练 env +2. Bias 使用 uni_test 值:[-5, +5](FIFO),[-5.1, +5.1](fade target) +3. Obs 归一化:`forces_norm = obs[6:12]/force_norm`,`sens_norm = (obs[0:6]-sens_dev)/sens_norm`,`hstack([forces_norm, sens_norm])` +4. 25-step fade-in / 25-step fade-out + +**修正后验证**: + +| 场景 | sim | cross-corr | active front mean | dq_ctl RMS | +|------|:---:|:----------:|:-----------------:|:----------:| +| vortex_lamb | 0.946 | 0.974 | 0.007 | 0.146 | +| vortex_taylor | 0.923 | 0.953 | -0.030 | 0.188 | + +**其他脚本审计**:`collect_karman.py` 和 `collect_illusion.py` 使用 `build_observation()` 函数,该函数正确实现了 force-first 归一化,无需修改。`collect_vortex.py` 是唯一手动构建 obs 的脚本。 + +**代码注释**:`collect_vortex.py` 的文件头 docstring 和关键行号均有 BUG HISTORY 和 BUG-FIX 标记。 diff --git a/src/CCD_analysis/correction_analysis/compare_dqctl_scenes.py b/src/CCD_analysis/correction_analysis/compare_dqctl_scenes.py new file mode 100644 index 0000000..f420a65 --- /dev/null +++ b/src/CCD_analysis/correction_analysis/compare_dqctl_scenes.py @@ -0,0 +1,188 @@ +"""Generate comparison figures across all cloak & illusion scenarios. + +All dq_ctl fields use unified geometry (pinball center at ~613px, sensors at ~800px), +set during GPU collection (configs.py UNIFIED coordinates). +Figures zoom into the region around the pinball/cylinder (x=300-1100) to exclude +boundary artifacts. + +1. All-scenes panorama: steady_cloak, karman_re100, vortex_lamb, vortex_taylor, + illusion_0.75L, illusion_1.0L, illusion_1.5L +2. Illusion-only comparison: 0.75L, 1.0L, 1.5L + +Usage: + conda run -n pycuda_3_10 python correction_analysis/compare_dqctl_scenes.py +""" +from __future__ import annotations + +import os +import sys + +import numpy as np +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt + +_SRC = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +if _SRC not in sys.path: + sys.path.insert(0, _SRC) + +from CCD_analysis.configs import DATA_DIR, NX, NY +from CCD_analysis.correction_analysis.compute_correction_fields import ( + compute_correction, +) + +FIG_DIR = os.path.join(DATA_DIR, "figures") +os.makedirs(FIG_DIR, exist_ok=True) + +# Display crop region (pixels) — around pinball at x~613 +CROP_X0, CROP_X1 = 300, 1100 + +# Scene groups +CLOAK_SCENES = ["steady_cloak", "karman_re100", "vortex_lamb", "vortex_taylor"] +ILLUSION_SCENES = ["illusion_0.75L", "illusion_1.0L", "illusion_1.5L"] +ALL_SCENES = CLOAK_SCENES + ILLUSION_SCENES + +SCENE_LABELS = { + "steady_cloak": "Steady Cloak", + "karman_re100": "Karman Cloak", + "vortex_lamb": "Vortex Lamb", + "vortex_taylor": "Vortex Taylor", + "illusion_0.75L": "Illusion 0.75L", + "illusion_1.0L": "Illusion 1.0L", + "illusion_1.5L": "Illusion 1.5L", +} + +FIELD_METRICS = [ + ("ux_mean", r"mean $u_x$", "RdBu_r", True), + ("uy_mean", r"mean $u_y$", "RdBu_r", True), + ("rms", "RMS", "viridis", False), + ("vorticity", r"$\omega_z$", "RdBu_r", True), +] + + +def compute_metrics(st: str) -> dict | None: + """Load dq_ctl for a scene and compute metrics.""" + try: + corr = compute_correction(st) + dq = corr.get("dq_ctl") + if dq is None: + return None + ux, uy = dq["ux"], dq["uy"] + return { + "ux_mean": np.mean(ux, axis=0), + "uy_mean": np.mean(uy, axis=0), + "rms": np.sqrt(np.std(ux, axis=0)**2 + np.std(uy, axis=0)**2), + "vorticity": np.gradient(np.mean(uy, axis=0), axis=1) + - np.gradient(np.mean(ux, axis=0), axis=0), + } + except Exception as e: + print(f" SKIP {st}: {e}") + return None + + +def crop_field(f: np.ndarray) -> np.ndarray: + """Crop to display region (NY, NX_cropped).""" + return f[:, CROP_X0:CROP_X1] + + +def plot_comparison(scene_list: str | list, name: str): + """Generate a grid of dq_ctl metrics for selected scenes.""" + if isinstance(scene_list, str): + scene_list = [scene_list] + + # Load all fields + fields = {} + for st in scene_list: + print(f" Loading {st}...", flush=True) + m = compute_metrics(st) + if m is not None: + fields[st] = m + + n_scenes = len(fields) + if n_scenes == 0: + print(" No valid fields, skipping") + return + + scene_names = list(fields.keys()) + n_rows = len(FIELD_METRICS) + + # Compute global vmax per metric from CROPPED fields + metric_vmax = {} + for mkey, _, _, _ in FIELD_METRICS: + all_vals = np.concatenate( + [abs(crop_field(fields[s][mkey])).ravel() for s in fields]) + vmax = float(np.percentile(all_vals[np.isfinite(all_vals)], 99.5)) + metric_vmax[mkey] = max(vmax, 1e-12) + + fig, axes = plt.subplots(n_rows, n_scenes, + figsize=(3.5 * n_scenes, 3.0 * n_rows)) + if n_rows == 1: + axes = [axes] + if n_scenes == 1: + axes = [[a] for a in axes] + + nx_crop = CROP_X1 - CROP_X0 + extent = (CROP_X0, CROP_X1, 0, NY - 1) + + for row, (mkey, mlabel, cmap, symmetric) in enumerate(FIELD_METRICS): + for col, sn in enumerate(scene_names): + ax = axes[row][col] + f = crop_field(fields[sn][mkey]) + vmax = metric_vmax[mkey] + + kwargs = {"cmap": cmap, "origin": "lower", + "aspect": "equal", "extent": extent} + if symmetric: + kwargs["vmin"] = -vmax + kwargs["vmax"] = vmax + else: + kwargs["vmin"] = 0 + kwargs["vmax"] = vmax + + ax.imshow(f, **kwargs) + ax.tick_params(left=False, right=False, labelleft=False, + bottom=False, top=False, labelbottom=False) + + if row == 0: + ax.set_title(SCENE_LABELS.get(sn, sn), fontsize=10) + if col == 0: + ax.set_ylabel(mlabel, fontsize=10) + + plt.suptitle(f"dq_ctl: [{', '.join(SCENE_LABELS.get(s,s) for s in scene_names)}]", + fontsize=12, y=1.01) + plt.tight_layout() + path = os.path.join(FIG_DIR, name) + fig.savefig(path, dpi=150, bbox_inches="tight") + plt.close(fig) + print(f" Saved: {path}", flush=True) + + # Stats + print(f"\n --- RMS (cropped region) ---") + for sn in scene_names: + rms_crop = crop_field(fields[sn]["rms"]) + rms_val = float(np.sqrt(np.mean(rms_crop**2))) + print(f" {sn:22s}: RMS={rms_val:.6f}") + + +def main(): + print("=" * 60) + print("Comparison: dq_ctl across all cloak & illusion scenes") + print("=" * 60) + + # 1. All 7 scenes panorama + print("\n--- All 7 scenes panorama ---") + plot_comparison(ALL_SCENES, "corr_comparison_all_scenes.png") + + # 2. Illusion-only (3 diameters) + print("\n--- Illusion-only comparison ---") + plot_comparison(ILLUSION_SCENES, "corr_illusion_comparison_dqctl.png") + + # 3. Cloak-only (4 scenes) for reference + print("\n--- Cloak-only comparison ---") + plot_comparison(CLOAK_SCENES, "corr_cloak_comparison_dqctl.png") + + print("\nDone!") + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/CCD_analysis/correction_analysis/decompose_corrections.py b/src/CCD_analysis/correction_analysis/decompose_corrections.py index 776b3fc..b906e26 100644 --- a/src/CCD_analysis/correction_analysis/decompose_corrections.py +++ b/src/CCD_analysis/correction_analysis/decompose_corrections.py @@ -30,7 +30,9 @@ from CCD_analysis.correction_analysis.compute_correction_fields import ( R_CANDIDATES = [6, 8, 10] CCD_Q = 6 -SCENE_TYPES = ["illusion_0.75L", "illusion_1.0L", "steady_cloak"] +SCENE_TYPES = ["illusion_0.75L", "illusion_1.0L", "illusion_1.5L", "steady_cloak"] +DIAMETERS_MAIN = [0.75, 1.0] +DIAMETER_SPECIAL = 1.5 # flagged as special_mechanism (high-freq modulation) def compute_modal_overlap(W_dict, scene_label, r, obs_label="force_fy"): @@ -101,7 +103,9 @@ def main(): continue diam = corr.get("diam") - print(f"\n--- {st} (diam={diam}) ---", flush=True) + is_special = (diam is not None and diam >= DIAMETER_SPECIAL) + flag = " [SPECIAL MECHANISM — high-freq modulation]" if is_special else "" + print(f"\n--- {st} (diam={diam}){flag} ---", flush=True) Q_ctl = dict_to_field_matrix(dq_ctl) N = Q_ctl.shape[1] @@ -146,6 +150,7 @@ def main(): "scene": st, "diam": diam, "obs": flabel, "r": r, "m80": m80, "N": sig.size, "sigma_top3": [float(sig[i]) for i in range(min(3,len(sig)))], + "special_mechanism": is_special, } if fmode == "fy": print(f" {key}: m80={m80} s1={float(sig[0]):.4f}", flush=True) @@ -163,6 +168,7 @@ def main(): "scene": st, "diam": diam, "obs": "action", "r": r, "m80": m80, "N": sig.size, "sigma_top3": [float(sig[i]) for i in range(min(3,len(sig)))], + "special_mechanism": is_special, } print(f" {key}: m80={m80} s1={float(sig[0]):.4f}", flush=True) @@ -181,6 +187,7 @@ def main(): "m80": int(np.searchsorted(cumulative_energy(sig_t), 0.80)+1) if len(sig_t) > 0 else 0, "N": sig_t.size, "sigma_top3": [float(sig_t[i]) for i in range(min(3,len(sig_t)))], + "special_mechanism": is_special, } # Overlap: dq_ctl vs dq_tar ck = f"{st}_dqctl_force_fy_r{r}" diff --git a/src/CCD_analysis/correction_analysis/diagnose_corrections.py b/src/CCD_analysis/correction_analysis/diagnose_corrections.py index e9e4da3..b388871 100644 --- a/src/CCD_analysis/correction_analysis/diagnose_corrections.py +++ b/src/CCD_analysis/correction_analysis/diagnose_corrections.py @@ -47,46 +47,27 @@ SCENE_TYPES = [ # --------------------------------------------------------------------------- -# Three-zone masks +# Three-zone masks (unified geometry: pinball center at 613 px, sensors at 800 px) # --------------------------------------------------------------------------- -def define_zones_illusion() -> dict: - """Define three-zone masks for illusion layout (sensors at x=30*L0).""" +def define_zones() -> dict: + """Define three-zone masks for all scenes (unified geometry, 2026-06-28). + + All scenes now use the same pinball/sensor positions after unified collection. + Zone ranges: + near_body: 580-720 px (around pinball at x≈613) + body_wake: 720-850 px (near wake downstream) + sensor_zone: 780-850 px (around sensors at x=800) + """ zones = {} - # Zone 1: near-body — envelope around pinball cylinders - # pinball front at x=380 (19*L0), rear at x=406 (20.3*L0) - # Extend to x=[350, 500], full height - mask = np.zeros((NY, NX), dtype=bool) - mask[:, 350:500] = True - zones["near_body"] = mask - - # Zone 2: body-connected near wake — immediate downstream - mask = np.zeros((NY, NX), dtype=bool) - mask[:, 500:700] = True - zones["body_wake"] = mask - - # Zone 3: downstream sensor zone — around sensors at x=600 - mask = np.zeros((NY, NX), dtype=bool) - mask[:, 580:650] = True - zones["sensor_zone"] = mask - - return zones - - -def define_zones_karman() -> dict: - """Define three-zone masks for Karman layout (sensors at x=40*L0=800).""" - zones = {} - # Zone 1: near-body — around pinball at x=600 mask = np.zeros((NY, NX), dtype=bool) mask[:, 580:720] = True zones["near_body"] = mask - # Zone 2: body-connected near wake mask = np.zeros((NY, NX), dtype=bool) mask[:, 720:850] = True zones["body_wake"] = mask - # Zone 3: downstream sensor zone — around sensors at x=800 mask = np.zeros((NY, NX), dtype=bool) mask[:, 780:850] = True zones["sensor_zone"] = mask @@ -272,8 +253,7 @@ def run(): print("Phase 2: Baseline Diagnostics (correction fields)", flush=True) print("=" * 60, flush=True) - zones_ill = define_zones_illusion() - zones_karman = define_zones_karman() + zones = define_zones() all_metrics = {} @@ -292,11 +272,7 @@ def run(): print(f" SKIP: no valid data (N=0)", flush=True) continue - # Determine which zones to use - is_illusion = "illusion" in scene_type - is_karman = "karman" in scene_type or "vortex" in scene_type - zones = zones_ill if is_illusion else (zones_karman if is_karman else zones_ill) - + # Unified geometry — same zones for all scenes for dq_key, dq_label in [ ("dq_blk", "dq_blk (pinball blockage)"), ("dq_ctl", "dq_ctl (control correction)"), @@ -313,8 +289,8 @@ def run(): metrics = zone_metrics(dq, zones, dq_label) all_metrics[f"{scene_type}_{dq_key}"] = metrics - # For illusion/karman/vortex, also plot dq_tar if available - if dq_key == "dq_ctl" and (is_illusion or is_karman): + # For scenes with a target, also plot dq_tar if available + if dq_key == "dq_ctl" and corr.get("dq_tar") is not None: dq_tar = corr.get("dq_tar") if dq_tar is not None: plot_mean_rms(dq_tar, "dq_tar (target correction)", prefix, zones) diff --git a/src/CCD_analysis/correction_analysis/visualize_action_ccd.py b/src/CCD_analysis/correction_analysis/visualize_action_ccd.py new file mode 100644 index 0000000..1cf2e13 --- /dev/null +++ b/src/CCD_analysis/correction_analysis/visualize_action_ccd.py @@ -0,0 +1,126 @@ +"""Action-CCD mode 1 visualization for cloak scenes. + +Action-CCD finds correction-field structures most correlated with cylinder +rotation speeds. For cloak scenes (steady/karman/vortex), this should reveal +the structures the controller directly modulates — clean velocity deficit +compensation and cylinder dipoles, excluding upstream disturbance structures. + +Usage: + conda run -n pycuda_3_10 python correction_analysis/visualize_action_ccd.py +""" +from __future__ import annotations + +import os +import sys + +import numpy as np +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt + +_SRC = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +if _SRC not in sys.path: + sys.path.insert(0, _SRC) + +from CCD_analysis.configs import DATA_DIR, NX, NY, L0 +from CCD_analysis.utils.resampling import ( + compute_pod, cumulative_energy, e95_index, compute_reduced_ccd, + unstack_velocity_modes, +) +from CCD_analysis.correction_analysis.compute_correction_fields import ( + compute_correction, dict_to_field_matrix, +) + +FIG_DIR = os.path.join(DATA_DIR, "figures") +os.makedirs(FIG_DIR, exist_ok=True) + +CLOAK_SCENES = ["steady_cloak", "vortex_lamb", "vortex_taylor"] +# karman_re100 excluded due to 72 vs 96 frame mismatch + +R = 10 +CCD_Q = 6 +CROP_X0, CROP_X1 = 300, 1100 + + +def main(): + print("=" * 60) + print("Action-CCD Mode 1: Cloak scenes") + print("=" * 60) + + for st in CLOAK_SCENES: + print(f"\n--- {st} ---", flush=True) + + # Load correction fields + corr = compute_correction(st) + dq_ctl = corr.get("dq_ctl") + if dq_ctl is None or dq_ctl.get("actions") is None: + print(f" SKIP: no dq_ctl or no actions") + continue + + # Build snapshot matrix and compute POD + Q = dict_to_field_matrix(dq_ctl) + N = Q.shape[1] + mf, modes, sv, coeffs = compute_pod(Q) + e95 = e95_index(cumulative_energy(sv)) + print(f" POD: E95={e95}, N_modes={len(sv)}") + + # Action-CCD: find structures correlated with actions + a_r = coeffs[:R, :] + actions = dq_ctl["actions"][:N].T # (3, N) + W, sigma, _, _, _, _ = compute_reduced_ccd(a_r, actions, Q_delay=CCD_Q) + + print(f" Action-CCD: sigma[0]={sigma[0]:.4f}, sigma_top3={sigma[:3]}") + + # Reconstruct CCD mode 1 in physical space + # z1 = W[:, 0] @ A_z → CCD temporal coefficient + # CCD mode = sum over POD modes of (CCD direction weights * POD mode) + w1 = W[:, 0] / (np.linalg.norm(W[:, 0]) + 1e-12) + ccd_mode1 = modes[:, :R] @ w1 # (2*NX*NY,) + + # Unstack into ux, uy + half = NX * NY + ux_mode = ccd_mode1[:half].reshape(NY, NX) + uy_mode = ccd_mode1[half:].reshape(NY, NX) + + # Plot mode 1: ux + uy + vorticity, cropped + vor = np.gradient(uy_mode, axis=1) - np.gradient(ux_mode, axis=0) + + fig, axes = plt.subplots(1, 3, figsize=(14, 4)) + extent = (CROP_X0, CROP_X1, 0, NY - 1) + + # ux + vmax_ux = max(abs(ux_mode).max(), 1e-12) + axes[0].imshow(ux_mode[:, CROP_X0:CROP_X1], cmap="RdBu_r", + vmin=-vmax_ux, vmax=vmax_ux, + origin="lower", aspect="equal", extent=extent) + axes[0].set_title(f"{st}: Action-CCD mode 1 ux") + + # uy + vmax_uy = max(abs(uy_mode).max(), 1e-12) + axes[1].imshow(uy_mode[:, CROP_X0:CROP_X1], cmap="RdBu_r", + vmin=-vmax_uy, vmax=vmax_uy, + origin="lower", aspect="equal", extent=extent) + axes[1].set_title(f"{st}: Action-CCD mode 1 uy") + + # vorticity + vmax_vor = max(np.percentile(abs(vor), 99), 1e-12) + axes[2].imshow(vor[:, CROP_X0:CROP_X1], cmap="RdBu_r", + vmin=-vmax_vor, vmax=vmax_vor, + origin="lower", aspect="equal", extent=extent) + axes[2].set_title(f"{st}: Action-CCD mode 1 vorticity") + + for ax in axes: + ax.tick_params(left=False, right=False, labelleft=False, + bottom=False, top=False, labelbottom=False) + + plt.tight_layout() + path = os.path.join(FIG_DIR, f"action_ccd_mode1_{st}.png") + fig.savefig(path, dpi=150, bbox_inches="tight") + plt.close(fig) + print(f" Saved: {path}") + + print("\nDone!") + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/CCD_analysis/scripts/collect_vortex.py b/src/CCD_analysis/scripts/collect_vortex.py new file mode 100644 index 0000000..5e56cac --- /dev/null +++ b/src/CCD_analysis/scripts/collect_vortex.py @@ -0,0 +1,509 @@ +"""Collect vortex cloak data for CCD correction-field analysis. + +Collects field snapshots for three scene types per vortex type: + - vortex_target_{type}: vortex only (no pinball), the "ideal" target flow + - vortex_uncontrolled_{type}: vortex + pinball, zero control (q_blk) + - vortex_{type}: vortex + pinball + PPO control (q_ctl) + +All use LegacyCelerisLab (matching existing CCD data convention). + +Usage: + conda run -n pycuda_3_10 python scripts/collect_vortex.py \\ + --type lamb --device 2 + conda run -n pycuda_3_10 python scripts/collect_vortex.py \\ + --type taylor --device 2 + conda run -n pycuda_3_10 python scripts/collect_vortex.py \\ + --type all --device 2 + +--- BUG HISTORY (2026-06-29) --- +Three independent bugs in this script were discovered and fixed after +Lamb dipole showed non-physical front cylinder oscillation: + +BUG 1 - Cylinder order swap (lines 166-168, 254-257): + Original training env adds cylinders: front(id3) -> TOP(+y, id4) -> BOTTOM(-y, id5). + action = [aF, aT(+bias), aB(-bias)] -> temp[3]=aF, temp[4]=aT(+bias), temp[5]=aB(-bias). + We had TOP and BOTTOM swapped, so bias -4 went to the wrong cylinder. + Consequence: rear cylinders rotated in opposite directions; front overcompensated. + +BUG 2 - Observation normalization swap (lines 368-371): + Training env produces obs = [forces/force_norm, sensors/sens_norm] (force-first). + We incorrectly fed [sensors/force_norm, forces/sens_norm] (channel-swapped + wrong norms). + Consequence: model received garbage feedback; on Lamb, similarity appeared 0.94 from + the similarity computation but cross-correlation was only 0.73. + +BUG 3 - Missing fade-in/out transitions (lines 314-357): + uni_test.ipynb uses 25-step fade-in from steady-cloak bias [-5.1U0, +5.1U0] to PPO action, + and 25-step fade-out back to steady-cloak. We applied PPO action immediately at full scale. + Consequence: flow instability from abrupt control changes. + +Fix summary: + - Cylinder order: TOP(+y) at id4, BOTTOM(-y) at id5. + - Bias: [-5, +5] U0 for both Lamb and Taylor (matching uni_test). + - Observation: forces_norm=obs[6:12]/force_norm_fact, sens_norm=(obs[0:6]-sens_dev)/sens_norm_fact. + Assembled as hstack([forces_norm, sens_norm]) — force first. + - Fade-in/out: 25 steps linear interpolation between steady_bias and PPO action. + +Comparison with other collect scripts: + collect_karman.py and collect_illusion.py use build_observation() from cfd_interface.py + which does force-first correctly. Only this script built obs manually. +""" +from __future__ import annotations + +import argparse +import json +import os +import sys +import time +from collections import deque + +import numpy as np + +_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) +if _REPO not in sys.path: + sys.path.insert(0, _REPO) +_SRC = os.path.join(_REPO, "src") +if _SRC not in sys.path: + sys.path.insert(0, _SRC) + +from LegacyCelerisLab import FlowField + +from CCD_analysis.configs import ( + get_scene, data_dir_for_scene, model_path_for_scene, + LEGACY_CFG_DIR, L0, CENTER_Y, U0, +) +from CCD_analysis.utils.cfd_interface import ( + load_legacy_configs, get_velocity_field, + load_ppo_model, +) + +DATA_TYPE = np.float32 +FIFO_LEN = 150 + + +# --------------------------------------------------------------------------- +# Vortex configuration +# --------------------------------------------------------------------------- + +_VORTEX_CFG = { + "lamb": {"vortex_type": "lamb", "vortex_strength": 0.5 * U0}, + "taylor": {"vortex_type": "taylor", "vortex_strength": 0.03 * U0}, +} + + +# --------------------------------------------------------------------------- +# Target: vortex only (no pinball), save fields.npz +# --------------------------------------------------------------------------- + +def collect_target(vtype: str, device_id: int, out_dir: str, + n_steps: int = 150) -> dict: + """Collect field snapshots for vortex-only target flow. + + Records the vortex evolving through a sensor-only environment + at x=40*L0. Saves fields.npz and sensors.npz. + """ + cfg = _VORTEX_CFG[vtype] + cuda_cfg, field_cfg = load_legacy_configs(LEGACY_CFG_DIR) + field_cfg = field_cfg._replace(viscosity=float(0.004)) + + ff = FlowField(field_cfg, cuda_cfg, device_id=device_id) + ny = ff.FIELD_SHAPE[1] + n_sensors = 3 + + # Add 3 sensors at x=40*L0 + sensor_positions = [2.0, 0.0, -2.0] + for y_off in sensor_positions: + ff.add_sensor((40.0 * L0, CENTER_Y + y_off * L0, 0.0), L0 / 4.0) + + # Short stabilization (1xNX/U0 for vortex, not 4x) + stabilize_steps = int(1 * ff.FIELD_SHAPE[0] / U0) + ff.run(stabilize_steps, np.zeros(n_sensors, dtype=DATA_TYPE)) + + # Save clean flow DDF + ff.get_ddf() + ff.save_ddf() + + # Add vortex at x=10*L0 + vc = cfg["vortex_type"] + vs = cfg["vortex_strength"] + ff.add_vortex((10.0 * L0, CENTER_Y, 0.0), + 2.0 * L0, vs, 0, vc) + + # Record vortex evolution + sens_list, forc_list = [], [] + ux_list, uy_list = [], [] + + for step in range(n_steps): + ff.run(800, np.zeros(n_sensors, dtype=DATA_TYPE)) + obs = ff.obs.copy() # (n_sensors*2,) = 6 sensor channels + sens_list.append(obs) + forc_list.append(np.zeros(6, dtype=DATA_TYPE)) # placeholder + ux, uy = get_velocity_field(ff, u0=U0) + ux_list.append(ux) + uy_list.append(uy) + + # Save + np.savez_compressed(os.path.join(out_dir, "fields.npz"), + ux=np.stack(ux_list), uy=np.stack(uy_list)) + np.savez(os.path.join(out_dir, "sensors.npz"), + sensors=np.array(sens_list, dtype=np.float32), + forces=np.array(forc_list, dtype=np.float32)) + + del ff + return {"scene": f"vortex_target_{vtype}", "n_steps": n_steps} + + +# --------------------------------------------------------------------------- +# Uncontrolled: vortex + pinball, zero control (q_blk) +# --------------------------------------------------------------------------- + +def collect_uncontrolled(vtype: str, device_id: int, out_dir: str, + n_steps: int = 150) -> dict: + """Collect field snapshots for vortex + pinball with zero control. + + Records the transient interaction of vortex with the pinball. + Target phases are recorded alongside field snapshots. + """ + cfg = _VORTEX_CFG[vtype] + cuda_cfg, field_cfg = load_legacy_configs(LEGACY_CFG_DIR) + field_cfg = field_cfg._replace(viscosity=float(0.004)) + + ff = FlowField(field_cfg, cuda_cfg, device_id=device_id) + ny = ff.FIELD_SHAPE[1] + n_sensors = 3 + + # ---- Phase 1: Sensors + target recording ---- + for y_off in [2.0, 0.0, -2.0]: + ff.add_sensor((40.0 * L0, CENTER_Y + y_off * L0, 0.0), L0 / 4.0) + + stabilize_steps = int(1 * ff.FIELD_SHAPE[0] / U0) + ff.run(stabilize_steps, np.zeros(n_sensors, dtype=DATA_TYPE)) + + # Save clean DDF (pre-pinball, pre-vortex) + ff.get_ddf() + ff.save_ddf() + + # Record target (vortex only, for similarity reference) + ff.add_vortex((10.0 * L0, CENTER_Y, 0.0), + 2.0 * L0, cfg["vortex_strength"], 0, cfg["vortex_type"]) + target_states = np.empty((0, 6), dtype=DATA_TYPE) + for _ in range(min(FIFO_LEN, n_steps)): + ff.run(800, np.zeros(n_sensors, dtype=DATA_TYPE)) + target_states = np.vstack((target_states, ff.obs.copy())) + np.savez(os.path.join(out_dir, "target.npz"), target_states=target_states) + + # ---- Phase 2: Add pinball, record uncontrolled flow ---- + ff.restore_ddf() + ff.apply_ddf() + + # BUG-FIX (2026-06-29): cylinder order MUST match training env. + # Original env adds: front(3) -> TOP(+y,id4) -> BOTTOM(-y,id5). + # Swapping TOP/BOTTOM causes rear cylinders to rotate wrong direction. + ff.add_cylinder((30.0 * L0, CENTER_Y, 0.0), L0 / 2.0) # id 3: front + ff.add_cylinder((31.3 * L0, CENTER_Y + 0.75 * L0, 0.0), L0 / 2.0) # id 4: TOP (+y) + ff.add_cylinder((31.3 * L0, CENTER_Y - 0.75 * L0, 0.0), L0 / 2.0) # id 5: BOTTOM (-y) + + n_obj = ff.obs.size // 2 + assert n_obj == 6, f"Expected 6, got {n_obj}" + + # Bias action stabilization (matching uni_test: [-5, +5] for both Lamb and Taylor) + ff.run(stabilize_steps, np.zeros(n_obj, dtype=DATA_TYPE)) + ff.run(stabilize_steps, np.array([0.0, 0.0, 0.0, 0.0, -5.0 * U0, 5.0 * U0], dtype=DATA_TYPE)) + + # Add vortex at x=15*L0 + ff.add_vortex((15.0 * L0, CENTER_Y, 0.0), + 2.0 * L0, cfg["vortex_strength"], 0, cfg["vortex_type"]) + + # Record uncontrolled flow (zero actions on pinball) + sens_list, forc_list = [], [] + ux_list, uy_list = [], [] + + for step in range(n_steps): + # Zero control on pinball + ff.run(800, np.zeros(n_obj, dtype=DATA_TYPE)) + obs = ff.obs.copy() + sens_list.append(obs[0:6]) + forc_list.append(obs[6:12]) + ux, uy = get_velocity_field(ff, u0=U0) + ux_list.append(ux) + uy_list.append(uy) + + np.savez_compressed(os.path.join(out_dir, "fields.npz"), + ux=np.stack(ux_list), uy=np.stack(uy_list)) + np.savez(os.path.join(out_dir, "sensors.npz"), + sensors=np.array(sens_list, dtype=np.float32), + forces=np.array(forc_list, dtype=np.float32)) + + del ff + return {"scene": f"vortex_uncontrolled_{vtype}", "n_steps": n_steps} + + +# --------------------------------------------------------------------------- +# Controlled: vortex + pinball + PPO (q_ctl) +# --------------------------------------------------------------------------- + +def collect_controlled(vtype: str, device_id: int, out_dir: str, + n_steps: int = 150) -> dict: + """Collect field snapshots for vortex + pinball with PPO control. + + Loads the trained PPO model, runs inference, saves fields and telemetry. + Also saves norm.json and ddf/fifo checkpoints for possible replay. + """ + scene_name = f"vortex_{vtype}" + cfg_src = get_scene(scene_name) + cfg_v = _VORTEX_CFG[vtype] + cuda_cfg, field_cfg = load_legacy_configs(LEGACY_CFG_DIR) + field_cfg = field_cfg._replace(viscosity=float(0.004)) + + ff = FlowField(field_cfg, cuda_cfg, device_id=device_id) + ny = ff.FIELD_SHAPE[1] + n_sensors = 3 + + # Save config + with open(os.path.join(out_dir, "config.json"), "w") as f: + json.dump({k: str(v) if not isinstance(v, (int, float, list, bool)) else v + for k, v in cfg_src.items()}, f, indent=2) + + # ---- Phase 1: Sensor-only target recording with vortex ---- + for y_off in [2.0, 0.0, -2.0]: + ff.add_sensor((40.0 * L0, CENTER_Y + y_off * L0, 0.0), L0 / 4.0) + + stabilize_steps_short = int(1 * ff.FIELD_SHAPE[0] / U0) + ff.run(stabilize_steps_short, np.zeros(n_sensors, dtype=DATA_TYPE)) + + ff.get_ddf() + ff.save_ddf() + + ff.add_vortex((10.0 * L0, CENTER_Y, 0.0), + 2.0 * L0, cfg_v["vortex_strength"], 0, cfg_v["vortex_type"]) + + target_states = np.empty((0, 6), dtype=DATA_TYPE) + for _ in range(min(FIFO_LEN, n_steps)): + ff.run(800, np.zeros(n_sensors, dtype=DATA_TYPE)) + target_states = np.vstack((target_states, ff.obs.copy())) + np.savez(os.path.join(out_dir, "target.npz"), target_states=target_states) + + # ---- Phase 2: Add pinball, compute norm ---- + ff.restore_ddf() + ff.apply_ddf() + + # Object order MUST match training env: front(id3), TOP(+y,id4), BOTTOM(-y,id5) + ff.add_cylinder((30.0 * L0, CENTER_Y, 0.0), L0 / 2.0) # id 3: front + ff.add_cylinder((31.3 * L0, CENTER_Y + 0.75 * L0, 0.0), L0 / 2.0) # id 4: TOP (+y) + ff.add_cylinder((31.3 * L0, CENTER_Y - 0.75 * L0, 0.0), L0 / 2.0) # id 5: BOTTOM (-y) + + n_obj = ff.obs.size // 2 + assert n_obj == 6, f"Expected 6, got {n_obj}" + + # Stabilize with zero action, then bias action matching training env + ff.run(stabilize_steps_short, np.zeros(n_obj, dtype=DATA_TYPE)) + ff.run(stabilize_steps_short, np.array([0.0, 0.0, 0.0, 0.0, -5.0 * U0, 5.0 * U0], dtype=DATA_TYPE)) + + # Add vortex at x=15*L0 + ff.add_vortex((15.0 * L0, CENTER_Y, 0.0), + 2.0 * L0, cfg_v["vortex_strength"], 0, cfg_v["vortex_type"]) + + # Save DDF checkpoint (vortex at x=15, pinball stabilized with bias) + ff.get_ddf() + ff.save_ddf() + + # Norm collection (zero action on vortex+pinball) + fifo = deque(maxlen=FIFO_LEN) + for _ in range(FIFO_LEN): + ff.run(800, np.zeros(n_obj, dtype=DATA_TYPE)) + fifo.append(ff.obs.copy()) + + temp_states = np.array(fifo, dtype=DATA_TYPE) + force_norm_fact = 6.0 * float(np.max(np.abs(temp_states[:, 6:12]))) + sens_deviation = np.mean(temp_states[:, 0:6], axis=0).astype(DATA_TYPE) + sens_norm_fact = np.zeros(6, dtype=DATA_TYPE) + for i in range(6): + sens_norm_fact[i] = 5.0 * float(np.max(np.abs(temp_states[:, i] - sens_deviation[i]))) + + norm = { + "force_norm_fact": force_norm_fact, + "sens_deviation": sens_deviation.tolist(), + "sens_norm_fact": sens_norm_fact.tolist(), + "action_bias": [0.0, -4.0, 4.0], + } + with open(os.path.join(out_dir, "norm.json"), "w") as f: + json.dump(norm, f, indent=2) + + # Bias FIFO init (restore DDF so vortex starts from x=15) + ff.restore_ddf() + ff.apply_ddf() + fifo.clear() + # BUG-FIX (2026-06-29): use uni_test bias [-5,+5], not training env bias [-4,+4]. + # The model was tested with [-5,+5] in uni_test.ipynb. + bias_fifo_arr = np.array([0.0, 0.0, 0.0, 0.0, -5.0 * U0, 5.0 * U0], dtype=DATA_TYPE) + for _ in range(FIFO_LEN): + ff.run(800, bias_fifo_arr) + fifo.append(ff.obs.copy()) + + save_states = np.array(list(fifo), dtype=DATA_TYPE) + # Restore DDF back to vortex-at-x=15 checkpoint + ff.restore_ddf() + ff.apply_ddf() + + # Save checkpoints for replay (vortex at initial position) + np.save(os.path.join(out_dir, "ddf_checkpoint.npy"), ff.ddf) + np.save(os.path.join(out_dir, "fifo_checkpoint.npy"), save_states) + + # ---- Phase 3: Controlled PPO inference (fade-in/out matching uni_test) ---- + model_path = model_path_for_scene(scene_name) + if model_path is None: + raise FileNotFoundError(f"No model found for scene: {scene_name}") + + model = load_ppo_model(model_path, device=f"cuda:{device_id}", s_dim=12) + model.set_random_seed(0) + + # Restore DDF to vortex-at-x=15 checkpoint + ff.restore_ddf() + ff.apply_ddf() + + # Start with zeros observation (matching uni_test) + obs = np.zeros(12, dtype=np.float32) + + sens_list, forc_list, act_list, rew_list = [], [], [], [] + ux_list = [] + uy_list = [] + fifo = deque(maxlen=FIFO_LEN) + + # Steady-cloak bias for transition (matching uni_test: [-5.1, +5.1]) + # BUG-FIX (2026-06-29): uni_test uses [-5.1,+5.1], not training env [-4,+4]. + steady_bias = np.array([0.0, -5.1 * U0, 5.1 * U0], dtype=DATA_TYPE) + + for step in range(n_steps): + action, _ = model.predict(obs, deterministic=True) + action = action.astype(np.float32).flatten() + act_list.append(action.copy()) + + temp_action = np.array(action * 4.0 + np.array([0.0, -4.0, 4.0]), dtype=DATA_TYPE) + + # Fade-in (0-24), active (25-44), fade-out (45-69), steady-cloak (70+) + if step < 25: + w = step / 25.0 + temp_val = temp_action * w * U0 + steady_bias * (1.0 - w) + elif 45 <= step < 70: + w = (step - 45) / 25.0 + temp_val = temp_action * (1.0 - w) * U0 + steady_bias * w + elif step >= 70: + temp_val = steady_bias + else: + temp_val = temp_action * U0 + + temp = np.zeros(n_obj, dtype=DATA_TYPE) + temp[3:6] = temp_val + + ff.context.push() + ff.run(800, temp) + ff.context.pop() + + obs_slice = ff.obs.copy() + fifo.append(obs_slice) + sens_list.append(obs_slice[0:6]) + forc_list.append(obs_slice[6:12]) + + # Build observation for next step + # BUG-FIX (2026-06-29): force-first with CORRECT norms. + # OLD (broken): sens_raw = obs[0:6]/force_norm, force_raw = (obs[6:12]-sens_dev)/sens_norm + # obs = hstack([sens_raw, force_raw]) ← completely wrong! + # NEW (correct): forces_norm = obs[6:12]/force_norm, sens_norm = (obs[0:6]-sens_dev)/sens_norm + # obs = hstack([forces_norm, sens_norm]) ← force first + # Training env produces: obs = hstack([forces/force_norm, sensors/sens_norm]). + # collect_karman and collect_illusion use build_observation() which does this correctly. + forces_norm = obs_slice[6:12] / force_norm_fact + sens_norm = (obs_slice[0:6] - sens_deviation) / sens_norm_fact + obs = np.clip(np.hstack([forces_norm, sens_norm]), -1.0, 1.0).astype(np.float32) + + # Save field snapshot + ux, uy = get_velocity_field(ff, u0=U0) + ux_list.append(ux) + uy_list.append(uy) + + # Save field snapshots + np.savez_compressed(os.path.join(out_dir, "fields.npz"), + ux=np.stack(ux_list), uy=np.stack(uy_list)) + + # Save telemetry + np.savez(os.path.join(out_dir, "controlled.npz"), + sensors=np.array(sens_list, dtype=np.float32), + forces=np.array(forc_list, dtype=np.float32), + actions=np.array(act_list, dtype=np.float32)) + + # Compute similarity + from CCD_analysis.utils.cfd_interface import compute_similarity + states_arr = np.array(sens_list, dtype=np.float32) + n_align = min(states_arr.shape[0], target_states.shape[0]) + if n_align >= 30: + sim = compute_similarity(target_states, states_arr[:n_align], 30) + else: + sim = 0.0 + + result = {"scene": scene_name, "similarity": float(sim), "n_steps": n_steps} + with open(os.path.join(out_dir, "result.json"), "w") as f: + json.dump(result, f, indent=2) + + del ff, model + return result + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main(): + ap = argparse.ArgumentParser(description="Collect vortex cloak fields for CCD") + ap.add_argument("--type", type=str, default="lamb", + help='Vortex type: lamb, taylor, or "all"') + ap.add_argument("--device", type=int, default=2, help="GPU device ID") + ap.add_argument("--steps", type=int, default=150, + help="Number of recording steps (max 150 for transient)") + ap.add_argument("--skip-target", action="store_true", + help="Skip vortex_target collection") + ap.add_argument("--skip-uncontrolled", action="store_true", + help="Skip uncontrolled collection") + ap.add_argument("--skip-controlled", action="store_true", + help="Skip controlled collection") + args = ap.parse_args() + + if args.type.lower() == "all": + vtypes = ["lamb", "taylor"] + else: + vtypes = [args.type.lower()] + + t_start = time.time() + for vtype in vtypes: + print(f"\n{'=' * 60}") + print(f"Vortex type: {vtype}") + print(f"{'=' * 60}") + + # --- Target: vortex only --- + if not args.skip_target: + scene_name = f"vortex_target_{vtype}" + print(f"\n--- Collecting target: {scene_name} ---") + out_dir = data_dir_for_scene(scene_name) + r = collect_target(vtype, args.device, out_dir, args.steps) + print(f" Done: {r['scene']} -> {out_dir}") + + # --- Uncontrolled: vortex + pinball, zero control --- + if not args.skip_uncontrolled: + scene_name = f"vortex_uncontrolled_{vtype}" + print(f"\n--- Collecting uncontrolled: {scene_name} ---") + out_dir = data_dir_for_scene(scene_name) + r = collect_uncontrolled(vtype, args.device, out_dir, args.steps) + print(f" Done: {r['scene']} -> {out_dir}") + + # --- Controlled: vortex + pinball + PPO --- + if not args.skip_controlled: + scene_name = f"vortex_{vtype}" + print(f"\n--- Collecting controlled: {scene_name} ---") + out_dir = data_dir_for_scene(scene_name) + r = collect_controlled(vtype, args.device, out_dir, args.steps) + print(f" Done: {r['scene']} -> {out_dir} sim={r['similarity']:.4f}") + + elapsed = time.time() - t_start + print(f"\nTotal time: {elapsed:.1f}s") + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/CCD_analysis/utils/field_translate.py b/src/CCD_analysis/utils/field_translate.py new file mode 100644 index 0000000..f716435 --- /dev/null +++ b/src/CCD_analysis/utils/field_translate.py @@ -0,0 +1,113 @@ +"""Field translation (spatial shifting) utilities for CCD analysis. + +When computing correction fields across scenes with different object positions +(e.g., illusion pinball at x=393 vs cloak pinball at x=613), fields must be +translated so that reference points align before subtraction. + +All functions operate on field arrays with shape (NY, NX) or (N, NY, NX). +NX = 1280, NY = 512 in the standard grid. +""" +from __future__ import annotations + +import numpy as np + + +def translate_field_x(field: np.ndarray, shift_x: int, + fill_edge: bool = True) -> np.ndarray: + """Horizontally shift a field by a given number of pixels. + + Parameters + ---------- + field : (NY, NX) or (N, NY, NX) ndarray + Velocity field(s) in legacy (NY, NX) order. + shift_x : int + Positive = shift right, negative = shift left. + fill_edge : bool + If True, fill vacated columns with edge values (smooth). + If False, fill with zeros (creates boundary artifacts). + + Returns + ------- + shifted : ndarray with same shape as input. + """ + if shift_x == 0: + return field.copy() + + if field.ndim == 2: + ny, nx = field.shape + result = np.zeros_like(field) + shift = shift_x + if shift > 0: + result[:, shift:] = field[:, :-shift] + if fill_edge: + # Fill left vacated columns with leftmost column value + result[:, :shift] = field[:, :1] + else: + s = -shift + result[:, :-s] = field[:, s:] + if fill_edge: + # Fill right vacated columns with rightmost column value + result[:, -s:] = field[:, -1:] + return result + elif field.ndim == 3: + n, ny, nx = field.shape + result = np.zeros_like(field) + shift = shift_x + if shift > 0: + result[:, :, shift:] = field[:, :, :-shift] + if fill_edge: + # Broadcast leftmost column across vacated columns + result[:, :, :shift] = field[:, :, :1] + else: + s = -shift + result[:, :, :-s] = field[:, :, s:] + if fill_edge: + result[:, :, -s:] = field[:, :, -1:] + return result + else: + raise ValueError(f"Unsupported field ndim: {field.ndim}") + + +def translate_fields_dict(q: dict, shift_x: int) -> dict: + """Translate ux and uy fields in a data dict, leaving telemetry unchanged.""" + if shift_x == 0: + return q + return { + "ux": translate_field_x(q["ux"], shift_x), + "uy": translate_field_x(q["uy"], shift_x), + "forces": q.get("forces"), + "sensors": q.get("sensors"), + "actions": q.get("actions"), + "meta": {**q.get("meta", {}), "translated_by": shift_x}, + "step_indices": q.get("step_indices"), + } + + +# --------------------------------------------------------------------------- +# Reference positions (pixel coordinates) +# --------------------------------------------------------------------------- +# All scenes now use UNIFIED geometry: +# pinball at (30, 31.3) x L0 -> center = 613 px +# sensors at 40 x L0 +# target cylinder at 30.65 x L0 = 613 px (same as pinball center) +# field_translate kept for optional cross-comparison or future use. + +ILLUSION_REF_X = 613 # UNIFIED: was 393 +CLOAK_REF_X = 613 # unchanged +TARGET_CYL_REF_X = 613 # UNIFIED: was 400 (now same as pinball center) + + +def get_scene_ref_x(scene_name: str) -> int | None: + """Get the reference x-position (pinball/cylinder center) for a scene. + + Returns pixel coordinate, or None if no reference (e.g. target_channel). + """ + if "illusion" in scene_name: + return ILLUSION_REF_X + if "target_cylinder" in scene_name: + return TARGET_CYL_REF_X + if scene_name in ("pinball", "steady_cloak", "karman_re100", "karman_q_blk", + "vortex_lamb", "vortex_taylor", + "vortex_uncontrolled_lamb", "vortex_uncontrolled_taylor"): + return CLOAK_REF_X + return None diff --git a/src/CCD_analysis/utils/load_vortex_fields.py b/src/CCD_analysis/utils/load_vortex_fields.py new file mode 100644 index 0000000..bbf1bb6 --- /dev/null +++ b/src/CCD_analysis/utils/load_vortex_fields.py @@ -0,0 +1,114 @@ +"""Load vortex scene fields.npz format for transient CCD analysis. + +Vortex scenes (vortex_lamb, vortex_taylor, vortex_target_*, vortex_uncontrolled_*) +have field snapshots saved as raw fields.npz with shape (N, NX, NY) and no phase +plan (transient, not periodic). + +Converts to the same convention as load_aligned_fields(): + - Transposes fields from (N, NX, NY) -> (N, NY, NX) + - Loads telemetry from controlled.npz or sensors.npz + - Returns dict with identical key structure +""" +from __future__ import annotations + +import json +import os +from typing import Any + +import numpy as np + +from CCD_analysis.configs import DATA_DIR, NX, NY, SCENES + + +def load_vortex_fields(scene_name: str) -> dict: + """Load vortex scene field data from raw fields.npz. + + Parameters + ---------- + scene_name : str — one of the vortex scene names registered in configs. + + Returns + ------- + dict with same keys as load_aligned_fields(): + ux, uy : (N, NY, NX) ndarray — field snapshots (transposed) + forces : (N, 6) ndarray or None + sensors : (N, 6) ndarray or None + actions : (N, 3) ndarray or None + meta : dict with scene info + step_indices : list of int (sequential 0..N-1 for transient) + """ + if scene_name not in SCENES: + raise KeyError(f"Unknown scene: {scene_name}") + + cfg = SCENES[scene_name] + scene_id = cfg["scene_id"] + data_dir = os.path.join(DATA_DIR, scene_id, scene_name) + + if not os.path.isdir(data_dir): + raise FileNotFoundError(f"Vortex scene directory not found: {data_dir}") + + # -- fields.npz (native simulation order: NX first) -- + fields_path = os.path.join(data_dir, "fields.npz") + if not os.path.isfile(fields_path): + raise FileNotFoundError(f"{fields_path} not found") + + fd = np.load(fields_path) + ux_raw = fd["ux"] # (N, NX, NY) + uy_raw = fd["uy"] + N = ux_raw.shape[0] + fd.close() + + # Transpose (N, NX, NY) -> (N, NY, NX) to match load_aligned_fields convention + ux = np.ascontiguousarray(ux_raw.transpose(0, 2, 1)) + uy = np.ascontiguousarray(uy_raw.transpose(0, 2, 1)) + + # -- Telemetry (controlled.npz or sensors.npz) -- + tele_path = None + for p in [ + os.path.join(data_dir, "controlled.npz"), + os.path.join(data_dir, "sensors.npz"), + ]: + if os.path.isfile(p): + tele_path = p + break + + sensors, forces, actions = None, None, None + if tele_path is not None: + td = np.load(tele_path) + if "sensors" in td: + sensors = td["sensors"] # (N, 6) + if "forces" in td: + forces = td["forces"] # (N, 6) + if "actions" in td: + actions = td["actions"] # (N, 3) + td.close() + + # Verify N matches + if sensors is not None and sensors.shape[0] != N: + raise ValueError( + f"sensors ({sensors.shape[0]}) != fields ({N})" + ) + if forces is not None and forces.shape[0] != N: + raise ValueError( + f"forces ({forces.shape[0]}) != fields ({N})" + ) + + meta = { + "scene": scene_name, + "scene_id": scene_id, + "source": "vortex_transient", + "target_type": "transient", + "n_frames": N, + } + + result: dict[str, Any] = { + "ux": ux, + "uy": uy, + "forces": forces, + "actions": actions, + "sensors": sensors, + "meta": meta, + "step_indices": list(range(N)), # sequential — no phase plan + } + + return result diff --git a/src/OID_analysis/data/derived/comparison/illusion_0.75L.json b/src/OID_analysis/data/derived/comparison/illusion_0.75L.json index 75d1e70..edd0d15 100644 --- a/src/OID_analysis/data/derived/comparison/illusion_0.75L.json +++ b/src/OID_analysis/data/derived/comparison/illusion_0.75L.json @@ -4,10 +4,10 @@ "force-oid_m2": 0.43535277661654437, "force-oid_m3": 0.43535277661654437, "force-oid_m5": 0.43535277661654437, - "sig-oid_m1": 0.017340158959890415, - "sig-oid_m2": 0.30180744104636803, - "sig-oid_m3": 0.09790190212866957, - "sig-oid_m5": 0.02065845627823741, + "sig-oid_m1": -0.15117050299050566, + "sig-oid_m2": 0.3261349291477396, + "sig-oid_m3": 0.21184489728386308, + "sig-oid_m5": 0.025177590823196313, "sig-pcd_m1": -0.03521104267963627, "sig-pcd_m2": 0.20732705633252407, "sig-pcd_m3": 0.11988520616706469, @@ -18,21 +18,21 @@ "pod_m5": -3.0459602065033202 }, "future_sig": { - "force-oid_m1": 0.013511471239959334, - "force-oid_m2": 0.07098337174417249, - "force-oid_m3": 0.07098337174417249, - "force-oid_m5": 0.07098337174417249, - "sig-oid_m1": 0.3740715508827751, - "sig-oid_m2": 0.6608883811088201, - "sig-oid_m3": 0.5592259563419594, - "sig-oid_m5": 0.533343435056657, - "sig-pcd_m1": 0.20205641028110888, - "sig-pcd_m2": 0.4672590946761527, - "sig-pcd_m3": 0.4468990482184305, - "sig-pcd_m5": 0.41968732641466205, - "pod_m1": -0.2540973280602146, - "pod_m2": -0.0339567217960513, - "pod_m3": 0.054785729407538376, - "pod_m5": 0.3000378545113639 + "force-oid_m1": 0.02808159096495584, + "force-oid_m2": 0.16866913136184078, + "force-oid_m3": 0.16866913136184078, + "force-oid_m5": 0.16866913136184078, + "sig-oid_m1": 0.33101756431711893, + "sig-oid_m2": 0.5494672616793316, + "sig-oid_m3": 0.530079016273497, + "sig-oid_m5": 0.32401702949087063, + "sig-pcd_m1": 0.16309737465172808, + "sig-pcd_m2": 0.5183971881220766, + "sig-pcd_m3": 0.5204063038036019, + "sig-pcd_m5": 0.4265691501067544, + "pod_m1": -0.052450576630182974, + "pod_m2": 0.04555274170153097, + "pod_m3": 0.0926186828625095, + "pod_m5": 0.33872788235759366 } } \ No newline at end of file diff --git a/src/OID_analysis/data/derived/comparison/illusion_1.0L.json b/src/OID_analysis/data/derived/comparison/illusion_1.0L.json index 806bff9..cd90f99 100644 --- a/src/OID_analysis/data/derived/comparison/illusion_1.0L.json +++ b/src/OID_analysis/data/derived/comparison/illusion_1.0L.json @@ -4,10 +4,10 @@ "force-oid_m2": 0.6705941647225692, "force-oid_m3": 0.6705941647225692, "force-oid_m5": 0.6705941647225692, - "sig-oid_m1": -2.7646669027021566, - "sig-oid_m2": -2.539151608216361, - "sig-oid_m3": -1.47692206321327, - "sig-oid_m5": -1.5110272636915942, + "sig-oid_m1": -2.590735492534273, + "sig-oid_m2": -1.746340572676032, + "sig-oid_m3": -0.505449315098804, + "sig-oid_m5": 0.20254103672707288, "sig-pcd_m1": -1.6681874811094162, "sig-pcd_m2": -1.342076853642359, "sig-pcd_m3": 0.04249559758899473, @@ -18,21 +18,21 @@ "pod_m5": -0.09978939220221528 }, "future_sig": { - "force-oid_m1": -0.688583875677999, - "force-oid_m2": 0.0977498946249901, - "force-oid_m3": 0.0977498946249901, - "force-oid_m5": 0.0977498946249901, - "sig-oid_m1": 0.3400013732837159, - "sig-oid_m2": 0.5855599713349928, - "sig-oid_m3": 0.6757301882995801, - "sig-oid_m5": 0.6051731015609549, - "sig-pcd_m1": -0.045583209960261946, - "sig-pcd_m2": -0.07349660070560707, - "sig-pcd_m3": 0.534579564047348, - "sig-pcd_m5": 0.6365887267870092, - "pod_m1": -0.3737860307570295, - "pod_m2": -0.1596051084511593, - "pod_m3": 0.08266261398865987, - "pod_m5": -0.33155756162258626 + "force-oid_m1": 0.045801892403349205, + "force-oid_m2": 0.2629802484043238, + "force-oid_m3": 0.2629802484043238, + "force-oid_m5": 0.2629802484043238, + "sig-oid_m1": 0.49884985568656176, + "sig-oid_m2": 0.7228144172245711, + "sig-oid_m3": 0.800731534358897, + "sig-oid_m5": 0.7557779433603652, + "sig-pcd_m1": 0.15914778050377293, + "sig-pcd_m2": 0.41216776425952434, + "sig-pcd_m3": 0.7363453012643849, + "sig-pcd_m5": 0.8203723624009522, + "pod_m1": -0.21134397189909435, + "pod_m2": 0.3003051304179969, + "pod_m3": 0.46937683250351936, + "pod_m5": 0.38547893664057714 } } \ No newline at end of file diff --git a/src/OID_analysis/data/derived/comparison/illusion_1.5L.json b/src/OID_analysis/data/derived/comparison/illusion_1.5L.json index 9fb2748..9508824 100644 --- a/src/OID_analysis/data/derived/comparison/illusion_1.5L.json +++ b/src/OID_analysis/data/derived/comparison/illusion_1.5L.json @@ -4,10 +4,10 @@ "force-oid_m2": 0.6397818250190341, "force-oid_m3": 0.6397818250190341, "force-oid_m5": 0.6397818250190341, - "sig-oid_m1": 0.5371119596459986, - "sig-oid_m2": 0.5689626851549741, - "sig-oid_m3": 0.5480702090166246, - "sig-oid_m5": 0.49764490473273426, + "sig-oid_m1": -0.052064205105397096, + "sig-oid_m2": 0.5476475353492796, + "sig-oid_m3": 0.5104783631016963, + "sig-oid_m5": 0.5006514705153909, "sig-pcd_m1": 0.02922230950403174, "sig-pcd_m2": 0.4747650262191032, "sig-pcd_m3": 0.5480885671190363, @@ -18,21 +18,21 @@ "pod_m5": 0.5163077019241664 }, "future_sig": { - "force-oid_m1": 0.25720592794565883, - "force-oid_m2": 0.07069504954059229, - "force-oid_m3": 0.07069504954059229, - "force-oid_m5": 0.07069504954059229, - "sig-oid_m1": 0.3378310203787158, - "sig-oid_m2": 0.3147990569733715, - "sig-oid_m3": 0.34429262568108926, - "sig-oid_m5": 0.33509730308714486, - "sig-pcd_m1": -0.002980254539846315, - "sig-pcd_m2": 0.35229352094431515, - "sig-pcd_m3": 0.3046193933085676, - "sig-pcd_m5": 0.3332866518761705, - "pod_m1": -0.01505551568170228, - "pod_m2": 0.05972906227204257, - "pod_m3": 0.050175749864584104, - "pod_m5": 0.2244801105502782 + "force-oid_m1": 0.17421268011080493, + "force-oid_m2": 0.14691686742584273, + "force-oid_m3": 0.14691686742584273, + "force-oid_m5": 0.14691686742584273, + "sig-oid_m1": 0.5380880846698864, + "sig-oid_m2": 0.7415813766705678, + "sig-oid_m3": 0.7457945827838065, + "sig-oid_m5": 0.743474115106988, + "sig-pcd_m1": 0.45784877152463377, + "sig-pcd_m2": 0.6555116659772753, + "sig-pcd_m3": 0.6986463965973865, + "sig-pcd_m5": 0.7043777394599675, + "pod_m1": 0.43491763790324245, + "pod_m2": 0.5396007454438271, + "pod_m3": 0.5888838546919968, + "pod_m5": 0.675748834414234 } } \ No newline at end of file diff --git a/src/OID_analysis/data/derived/comparison/karman_re100.json b/src/OID_analysis/data/derived/comparison/karman_re100.json index cb7eee4..b782511 100644 --- a/src/OID_analysis/data/derived/comparison/karman_re100.json +++ b/src/OID_analysis/data/derived/comparison/karman_re100.json @@ -1,21 +1,21 @@ { "force": { - "force-oid_m1": 0.3973693481528069, - "force-oid_m2": 0.7503722371594272, - "force-oid_m3": 0.7503722371594272, - "force-oid_m5": 0.7503722371594272, - "sig-oid_m1": 0.047626492192117884, - "sig-oid_m2": -0.0899320785087113, - "sig-oid_m3": -0.06793031697290859, - "sig-oid_m5": 0.050723754778942164, - "sig-pcd_m1": -0.032869874061091105, - "sig-pcd_m2": -0.03470568581697929, - "sig-pcd_m3": -0.0024393867643178763, - "sig-pcd_m5": 0.20808527695100557, - "pod_m1": -0.028581812678008658, - "pod_m2": 0.41796895591108846, - "pod_m3": 0.3922853200314628, - "pod_m5": 0.5941700935980355 + "force-oid_m1": 0.11266023422325092, + "force-oid_m2": 0.29502997457263447, + "force-oid_m3": 0.29502997457263447, + "force-oid_m5": 0.29502997457263447, + "sig-oid_m1": -0.04664427253668195, + "sig-oid_m2": -0.1264864272302698, + "sig-oid_m3": -0.5831767238197324, + "sig-oid_m5": -0.468171814213285, + "sig-pcd_m1": -0.01403430119734832, + "sig-pcd_m2": -0.10619108268969935, + "sig-pcd_m3": -0.4769605173029764, + "sig-pcd_m5": -0.5750085233764719, + "pod_m1": -0.2578359217654613, + "pod_m2": 0.06838543401441043, + "pod_m3": -0.04891972521400708, + "pod_m5": 0.14664737200706499 }, "future_sig": { "force-oid_m1": 0.0, diff --git a/src/OID_analysis/data/derived/pod/karman_re100/summary.json b/src/OID_analysis/data/derived/pod/karman_re100/summary.json index cce48b2..e6b438a 100644 --- a/src/OID_analysis/data/derived/pod/karman_re100/summary.json +++ b/src/OID_analysis/data/derived/pod/karman_re100/summary.json @@ -2,13 +2,11 @@ "scene": "karman_re100", "n_snapshots": 500, "dof": 67200, - "ranks_computed": [ - 6, - 8, - 10, - 12, - 16 - ], - "energy_r10_5modes": 0.999034936679307, - "energy_r10_10modes": 1.0 + "roi": { + "x_start": 400, + "x_end": 1000, + "y_start": 100, + "y_end": 400 + }, + "energy_r10_5modes": 0.9971600541895967 } \ No newline at end of file diff --git a/src/OID_analysis/data/derived/whitebox/karman_re100.json b/src/OID_analysis/data/derived/whitebox/karman_re100.json index de3e22d..1d2c727 100644 --- a/src/OID_analysis/data/derived/whitebox/karman_re100.json +++ b/src/OID_analysis/data/derived/whitebox/karman_re100.json @@ -1,6 +1,6 @@ { - "obs_act_train": 0.9560973048210144, - "oid_m3_act_train": 0.22518758634108724, - "oid_m5_act_train": 0.22518758634108724, - "oid_force_act_train": 0.23275911247341877 + "obs_act_train": 0.7495932579040527, + "oid_m3_act_train": 0.09605772105033679, + "oid_m5_act_train": 0.09605772105033679, + "oid_force_act_train": 0.11671916873232673 } \ No newline at end of file diff --git a/src/OID_analysis/data/karman_cloak/karman_blk/norm.json b/src/OID_analysis/data/karman_cloak/karman_blk/norm.json index 7c4a56c..bfbecc7 100644 --- a/src/OID_analysis/data/karman_cloak/karman_blk/norm.json +++ b/src/OID_analysis/data/karman_cloak/karman_blk/norm.json @@ -1,20 +1,20 @@ { - "force_norm_fact": 0.019301448483020067, + "force_norm_fact": 0.0192322488874197, "sens_deviation": [ - 0.7973665595054626, - -0.12411565333604813, - 0.25158512592315674, - -0.008040801621973515, - 0.8032128214836121, - 0.11381910741329193 + 0.8234996795654297, + -0.12610766291618347, + 0.2525006830692291, + -0.0017381409415975213, + 0.7810041308403015, + 0.12196661531925201 ], "sens_norm_fact": [ - 2.679999828338623, - 3.1077914237976074, - 1.8671960830688477, - 3.329291582107544, - 2.9416096210479736, - 3.267305374145508 + 3.3339574337005615, + 3.35296630859375, + 1.8934848308563232, + 3.4226977825164795, + 3.0502705574035645, + 3.111417293548584 ], "action_bias": [ 0.0, diff --git a/src/OID_analysis/data/karman_cloak/karman_re100/config.json b/src/OID_analysis/data/karman_cloak/karman_re100/config.json index 1a643fa..c362e18 100644 --- a/src/OID_analysis/data/karman_cloak/karman_re100/config.json +++ b/src/OID_analysis/data/karman_cloak/karman_re100/config.json @@ -1,6 +1,7 @@ { "scene_id": "karman", "re_code": 100, + "nu": 0.004, "has_disturbance": true, "sample_interval": 800, "action_scale": 8.0, @@ -15,6 +16,5 @@ "pinball_rear_x": 31.3, "target_type": "periodic", "s_dim": 12, - "u0": 0.01, - "nu": 0.004 + "u0": 0.01 } \ No newline at end of file diff --git a/src/OID_analysis/data/karman_cloak/karman_re100/result.json b/src/OID_analysis/data/karman_cloak/karman_re100/result.json index 65cf1d0..2b48a82 100644 --- a/src/OID_analysis/data/karman_cloak/karman_re100/result.json +++ b/src/OID_analysis/data/karman_cloak/karman_re100/result.json @@ -1,5 +1,5 @@ { "scene": "karman_re100", - "similarity": 0.9587061844662661, - "avg_reward": 0.6636279821395874 + "n_steps": 500, + "similarity": 0.0 } \ No newline at end of file diff --git a/src/SR_analysis/PIPELINE.md b/src/SR_analysis/PIPELINE.md new file mode 100644 index 0000000..e5117a4 --- /dev/null +++ b/src/SR_analysis/PIPELINE.md @@ -0,0 +1,144 @@ +# SR Analysis Pipeline + +> Symbolic regression pipeline for extracting interpretable DRL control laws (obs → act) from the fluidic pinball. +> Four independent stages: inference → fitting → validation → analysis. + +## Pipeline Architecture + +``` +[PPO 推理] [PySR 拟合] [CFD 验证] [分析/画图] +stage_1_infer.py stage_2_fit.py stage_3_validate.py stage_4_analyze.py + ↓ ↓ ↓ ↓ +controlled.npz formulas/*.json validations/*.json data/figures/*.png +``` + +## Quick Start + +```bash +# 1. Generate PPO data +conda run -n pycuda_3_10 python stage_1_infer.py --scene karman_re100 --device 2 + +# 2. Fit PySR formula +conda run -n sr_env python stage_2_fit.py --scene karman_re100 --mode per-scene + +# 3. Validate in CFD +conda run -n pycuda_3_10 python stage_3_validate.py \ + --scene karman_re100 --device 2 --mode pysr \ + --formula-front results/formulas/karman_re100_front.json \ + --formula-top results/formulas/karman_re100_top.json + +# 4. Analyze results +conda run -n pycuda_3_10 python stage_4_analyze.py --scene karman_re100 --mode ppo-viz +``` + +## Environments + +| Env | Used For | +|-----|----------| +| `pycuda_3_10` | Stage 1 (CFD inference), Stage 3 (CFD validation), Stage 4 (analysis) | +| `sr_env` | Stage 2 (PySR symbolic regression) | + +GPU: device 2 recommended (device 0 may conflict with PyTorch). + +## Key Conventions + +### Reynolds Number +- Code Re uses reference length 2D = 40: `Re = U0 * 40 / nu` +- Physical Re_D uses D = 20: `Re_D = Re / 2` +- Default: Re_code=100 → Re_D=50, nu=0.004 + +### Action +- `controlled.npz` stores actions as **normalized [-1, +1]** (not physical omega) +- Physical omega: `omega = (action * scale + bias) * U0`, then divided by radius for angular velocity +- Fitting target: **non-dimensional alpha = omega / U0** (not omega) + +### Action Bias vs FIFO Bias +- **DRL action decoder bias**: `action * scale + bias` → physical omega. Karman: [0,-4,4], Illusion: [0,-2,2], Vortex: [0,-4,4] +- **FIFO initialization bias** (environment warmup): different values! Illusion FIFO uses [0, -U0, U0] (1U scale), not [0, -2U0, 2U0] + +### Inlet +- Parabolic velocity profile (not uniform). Top/bottom walls are no-slip bounce-back. +- U0 = 0.01 at centerline (lattice units) + +### G-mirror +- Correct: `[aF, aT, aB] → [-aF, -aB, -aT]` (not the old buggy version) +- v23 structure: Front no-bias (α_F = 0 when features = 0), rear shared-head (α_B = -Top∘G) + +### Norm +- Each scene computes its own force/sensor normalization during environment initialization +- Must use the **same norm values** during inference and during validation +- Norm values are saved in `norm.json` in each data directory + +### Sample Interval & Steps +| Scene | SI | Validation Steps | +|-------|:--:|:----------------:| +| Karman | 800 | 160-200 | +| Illusion 0.75L | 400 | 320 | +| Illusion 1L | 600 | 214 | +| Illusion 1.5L | 800 | 160 | +| Vortex | 800 | 150 (transient) | + +Rule: steps ≥ NX/U0/SI = 1280/0.01/SI ≈ 128000/SI + +## Results Index + +All results indexed in [`scene_registry.json`](scene_registry.json). Canonical formulas in `results/formulas/`, CFD validations in `results/validations/`. + +### Canonical Formulas + +| Formula File | Scene | Formula | +|-------------|-------|---------| +| `results/formulas/karman_joint_front.json` | Karman cross-Re (joint) | `daF_dt - 14.952*mu*Cl_tot` | +| `results/formulas/karman_joint_top.json` | Karman cross-Re (joint) | `3.414` (constant) | +| `results/formulas/illusion_joint_front.json` | Illusion joint (0.75L+1L) | `Cd_tot - (Cd_err + 5.428) - 0.00978*(du_a_dt + u_a)` | +| `results/formulas/illusion_joint_top.json` | Illusion joint (0.75L+1L) | `(Cd_err - (Cd_rear - Cl_err))*0.535 + 2.782` | + +### Key CFD Results + +| Scene | Formula | Similarity | +|-------|---------|:----------:| +| Karman cross-Re avg | Joint | 0.847 | +| Illusion 0.75L | Joint | 0.978 | +| Illusion 1L | Joint | 0.970 | +| Vortex lamb | Karman joint | 0.949 (exceeds PPO 0.942) | +| Illusion 0.6L | Joint (generalization) | 0.939 | +| Illusion 0.8L | Joint (generalization) | 0.908 | +| Illusion 1.2L | Joint (generalization) | 0.849 | +| Illusion 2L | Joint (generalization) | 0.676 | + +### Feature Sets + +| Name | Features | Dim | Used For | +|------|----------|:---:|----------| +| PHASE_STATE_KEYS | u_a, du_a_dt, Cl_tot, dCl_tot_dt, Cd_tot, Cd_rear | 6 | Karman per-Re | +| ILLUSION_PHASE_KEYS | phase-state + Cd_err, Cl_err, dCd_err_dt, dCl_err_dt | 10 | Illusion | +| PHYS_DADT | physics + daF_dt, daB_dt, daT_dt + mu | 17 | Karman joint/deep | + +## Key Documentation + +| File | Content | +|------|---------| +| `PIPELINE.md` | This file — overview, environment, conventions | +| `sindy_sr_knowledge.md` | Bug history, confirmed facts, known limitations | +| `sindy_sr_notes.md` | Task list, current status | +| `STAGE_1_INFER.md` | Stage 1: PPO data generation | +| `STAGE_2_FIT.md` | Stage 2: PySR fitting | +| `STAGE_3_VALIDATE.md` | Stage 3: CFD validation | +| `STAGE_4_ANALYZE.md` | Stage 4: Analysis & visualization | +| `docs/SR_analysis_report.md` | Full report (465+ lines) | +| `docs/illusion_joint_formula_analysis.md` | Illusion joint formula deep dive | + +## Stage 0 Audit (2026-06-28) + +All imports verified in both conda environments: + +| Script | pycuda_3_10 | sr_env | +|--------|:-----------:|:------:| +| `stage_1_infer.py` (infer_karman / infer_illusion / infer_vortex) | OK | — | +| `stage_2_fit.py` (PySR) | — | OK | +| `stage_3_validate.py` (closed-loop) | OK | — | +| `stage_4_analyze.py` (analysis) | OK | — | +| `core/features.py` (feature_builder) | OK | OK | +| `core/cfd.py` (cfd_interface) | OK | — | + +No broken imports. All dependencies available. diff --git a/src/SR_analysis/README.md b/src/SR_analysis/README.md index 6558657..3361218 100644 --- a/src/SR_analysis/README.md +++ b/src/SR_analysis/README.md @@ -4,181 +4,81 @@ Extracts interpretable control laws (`obs -> act`) from DRL-trained policies for fluidic pinball. Uses **PySR symbolic regression** on dimensionless physical features with G-equivariant structural constraints (v23: front no-bias, rear shared-head). -## Current Results (2026-06-25) +## Quick Start -### Karman Cloak — Cross-Re Unified Formula +```bash +# 1. Generate PPO data +conda run -n pycuda_3_10 python stage_1_infer.py --scene karman_re100 --device 2 -| Scene | Front Formula | Top Formula | CFD Closed-Loop | -|-------|--------------|-------------|:---------------:| -| Joint (Re50-400) | `daF_dt - 14.952*mu*Cl_tot` | `alpha_T = 3.414` (const) | **0.847 avg** | -| Re50 independent | PySR per-Re best | — | **0.895** | -| Re100 independent | PySR per-Re best | — | **0.888** | -| Re200 independent | PySR per-Re best | — | **0.916** | -| Re400 (SI=400 opt) | Joint formula | Joint formula | **0.819** | +# 2. Fit formula +conda run -n sr_env python stage_2_fit.py --scene karman_re100 --mode per-scene -### Illusion +# 3. Validate in CFD +conda run -n pycuda_3_10 python stage_3_validate.py \\ + --scene karman_re100 --device 2 --mode pysr \\ + --formula-front results/formulas/karman_joint_front.json \\ + --formula-top results/formulas/karman_joint_top.json -| Scene | Front Formula | CFD Closed-Loop | % of PPO | -|-------|--------------|:---------------:|:--------:| -| 0.75L | `-0.169*(Cl_tot + dCl_tot_dt) - 1.240` | **0.979** | 100.7% | -| 1L | `(du_a_dt + u_a + 26.5)*0.0123` | **0.957** | 98.4% | -| **Joint (0.75L+1L)** | `target_Cd - 5.428 + 0.0098*(du_a_dt + u_a)` | **0.978 / 0.970** | — | -| 1.5L | High-freq periodic modulation (not SR-amenable) | — | — | +# 4. Analyze +conda run -n pycuda_3_10 python stage_4_analyze.py --scene karman_re100 --mode ppo-viz +``` -**Key finding**: 0.75L and 1L formulas have fundamentally different skeletons (Cl_tot vs u_a -dominant). Joint formula still achieves excellent CFD results on both although the underlying -mechanisms differ. +## Pipeline Architecture -### Illusion Generalization (Joint Formula, No PPO) +``` +stage_1_infer.py → stage_2_fit.py → stage_3_validate.py → stage_4_analyze.py + (PPO数据) (PySR拟合) (CFD闭环验证) (分析/画图) +``` -| Diameter | Similarity | Notes | -|:--------:|:----------:|-------| -| 0.5L | 0.854 | Signal weak, noise-dominated | -| 0.6L | **0.939** | Generalizes well | -| 0.8L | **0.908** | Generalizes well | -| 1.2L | 0.849 | Begins to degrade | -| 1.5L | N/A | High-frequency regime, different mechanism | -| 2.0L | 0.676 | Degraded, near 1.5L regime | +## Directory Structure -Valid range: 0.6L-1.0L (similarity > 0.90). - -### Vortex Cloak (Generalization) - -Karman joint formula tested on vortex scenes (no retraining): -| Scene | Karman Joint Formula | PPO Baseline | -|-------|:-------------------:|:------------:| -| vortex_lamb | **0.949** | 0.942 | -| vortex_taylor | **0.905** | 0.916 | +``` +SR_analysis/ + PIPELINE.md # 总览文档 (入口) + README.md # 本文件 + sindy_sr_knowledge.md # 知识库 (bugs, 事实, 结果) + sindy_sr_notes.md # 任务清单 + scene_registry.json # 所有场景的规范结果索引 + configs.py # 场景注册表 + core/ # 共享工具库 + features.py # 特征构建 (无量纲化+phase-state) + fitting.py # STLSQ拟合+特征矩阵 + cfd.py # LegacyCelerisLab接口 + g_operator.py # G-mirror变换 + data/ # 运行时生成的.npz数据 + results/ + formulas/ # 规范公式JSON + validations/ # CFD闭环验证结果 + archive/ # 归档的中间文件 + stage_1_infer.py # Stage 1: 统一PPO推理入口 + STAGE_1_INFER.md + stage_2_fit.py # Stage 2: 统一PySR拟合入口 + STAGE_2_FIT.md + stage_3_validate.py # Stage 3: 统一CFD闭环验证入口 + STAGE_3_VALIDATE.md + stage_4_analyze.py # Stage 4: 统一分析/画图入口 + STAGE_4_ANALYZE.md +``` --- -## Pipeline Overview - -``` -controlled.npz (PPO rollout) - | - v -compute_features() --> dimensionless physics features (ILLUSION_PHASE_KEYS, etc.) - | - v -PySR symbolic regression --> sparse interpretable formulas - | - v -CFD closed-loop validation --> final similarity score -``` - -### Key Design Decisions +## Key Design Decisions 1. **Feature levels**: Static (8-dim) -> Phase-state (6-dim) -> Illusion-phase (10-dim) 2. **Output target**: Non-dimensional alpha, not physical omega 3. **v23 structure**: Front no-bias, rear shared-head (Bottom = -Top(Gx)) 4. **Final judge**: CFD closed-loop similarity, not one-step R2 ---- - -## Directory Structure - -``` -SR_analysis/ - configs.py # Scene metadata (Karman, Illusion, Vortex) - configs/legacy/ # Legacy CFD configs (config_cuda.json, config_flowfield.json) - utils/ - __init__.py # Exports (no pycuda dependency) - feature_builder.py # Dimensionless features, G-operator, phase-state features - sindy_fitter.py # STLSQ fitting + feature matrices - cfd_interface.py # LegacyCelerisLab wrapper (requires pycuda_3_10) - g_operator.py # Equivariance diagnostics - data/ # Inference output data (controlled.npz, target.npz) - karman/ karman_re50..400/ - illusion/ illusion_0.75L,1L,1.5L/ - vortex/ vortex_lamb,taylor/ - scripts/ - infer_karman.py # PPO inference -> controlled.npz - infer_illusion.py # PPO inference -> controlled.npz - infer_vortex.py # PPO inference -> controlled.npz - gen_illusion_target.py # Target data generation for generalization scenes - visualize_ppo_illusion.py# PPO visualization with vorticity - sindy/ - run_pysr.py # PySR symbolic regression (niter=40) - run_pysr_deep.py # Karman deep PySR (niter=120, Re independent + joint) - run_pysr_deep_illusion.py# Illusion deep+joint PySR (niter=120) - validate/ - run_closed_loop.py # Karman closed-loop validator - run_closed_loop_illusion.py # Illusion closed-loop validator - run_closed_loop_vortex.py # Vortex closed-loop validator - run_closed_loop_re400_si.py # Karman re400 short-SI validator - predict_pysr.py # PySR formula sympy.lambdify wrapper - eval_rollout.py # Offline multi-step rollout evaluation - launch_pysr_validation.py # Batch CFD validation launcher - batch_illusion_generalization.sh# Batch generalization CFD validation - results/ # 136 JSON files — canonical + intermediate - results/README.md # Result file reference table - results/archive/ # Archived intermediate search attempts -``` - ---- - -## Usage - -### PySR Symbolic Regression (conda: sr_env) - -```bash -# Illusion -conda run -n sr_env python src/SR_analysis/sindy/run_pysr_deep_illusion.py --individual - -# Karman deep (cross-Re independent + joint) -conda run -n sr_env python src/SR_analysis/sindy/run_pysr_deep.py --both -``` - -### CFD Closed-Loop Validation (conda: pycuda_3_10, GPU 1 or 2) - -```bash -# Illusion PySR formula -conda run -n pycuda_3_10 python src/SR_analysis/validate/run_closed_loop_illusion.py \ - --scene illusion_1L --device 2 --steps 320 --mode pysr \ - --pysr-front validate/results/pysr_illusion_1L_front.json \ - --pysr-top validate/results/pysr_illusion_1L_top.json - -# Karman joint formula -conda run -n pycuda_3_10 python src/SR_analysis/validate/run_closed_loop.py \ - --scene karman_re100 --device 2 --steps 200 --mode pysr \ - --pysr-front validate/results/karman_joint_deep_front.json \ - --pysr-top validate/results/karman_joint_deep_top.json - -# Vortex (generalization test) -conda run -n pycuda_3_10 python src/SR_analysis/validate/run_closed_loop_vortex.py \ - --scene vortex_lamb --device 2 --steps 150 --mode pysr \ - --pysr-front validate/results/karman_joint_deep_front.json \ - --pysr-top validate/results/karman_joint_deep_top.json -``` - -### PPO Inference (generate controlled.npz) - -```bash -conda run -n pycuda_3_10 python src/SR_analysis/scripts/infer_karman.py --re 100 --device 2 -conda run -n pycuda_3_10 python src/SR_analysis/scripts/infer_illusion.py --diameter 1.0 --device 2 -conda run -n pycuda_3_10 python src/SR_analysis/scripts/infer_vortex.py --type lamb --device 2 -``` - ---- - -## Critical Reminders - -- **actions.npz are normalized [-1,1]**, not physical omega. Convert: `(action * scale + bias) * u0` -- **PySR needs `sensors_raw`/`forces_raw`** passed to `compute_features()` or derivative features are zero -- **Output target must be alpha** (non-dim): `Y = actions_phys / u0` -- **One-step R2 high != closed-loop good** -- always validate in CFD -- **Controls must propagate**: steps >= NX/u0/SI (S=400->320, S=600->214, S=800->160) -- **FIFO bias != DRL action bias** for Illusion: FIFO=[0,-U0,U0], decode=[0,-2,2]*U0 -- **Joint formula must be manually reviewed** for spurious terms (e.g. `daB_dt` is constant=0 at deployment) - ---- - ## Key Documentation | File | Content | |------|---------| -| `src/SR_analysis/sindy_sr_knowledge.md` | Background knowledge, bug history, known pitfalls (for coder reference) | -| `src/SR_analysis/sindy_sr_notes.md` | Task list, phase breakdown, current status | -| `docs/SR_analysis_report.md` | **Single consolidated report** — all formulas, results, methodology, structural analysis | -| `docs/illusion_joint_formula_analysis.md` | Illusion joint formula deep dive — physical interpretation, generalization curve | +| `PIPELINE.md` | **Primary entry** — pipeline overview, environment, conventions | +| `sindy_sr_knowledge.md` | Bug history, confirmed facts, known limitations | +| `sindy_sr_notes.md` | Task list, current status | +| `docs/SR_analysis_report.md` | Full report (465+ lines) | +| `docs/illusion_joint_formula_analysis.md` | Illusion joint formula deep dive | + +## Core Files (≤20) + +`stage_1_infer.py`, `stage_2_fit.py`, `stage_3_validate.py`, `stage_4_analyze.py`, `configs.py`, `scene_registry.json`, `core/features.py`, `core/fitting.py`, `core/cfd.py`, `core/g_operator.py` + 8 docs. diff --git a/src/SR_analysis/STAGE_1_INFER.md b/src/SR_analysis/STAGE_1_INFER.md new file mode 100644 index 0000000..7386ec9 --- /dev/null +++ b/src/SR_analysis/STAGE_1_INFER.md @@ -0,0 +1,31 @@ +# Stage 1: PPO Inference + +Generates `controlled.npz` data by running trained PPO models in LegacyCelerisLab CFD. + +## Usage + +```bash +conda run -n pycuda_3_10 python stage_1_infer.py --scene karman_re100 --device 2 +conda run -n pycuda_3_10 python stage_1_infer.py --group karman_trained --device 2 +conda run -n pycuda_3_10 python stage_1_infer.py --scene illusion_0.6L --target-only --device 2 +``` + +## Scene Groups + +karman_trained (re50-400), illusion_trained (0.75L/1L/1.5L), illusion_generalization (0.5L-2L), vortex_all. + +## Output per Scene + +`data/{scene_id}/{scene_name}/`: target.npz, controlled.npz, config.json, norm.json, result.json, target_harmonics.json (Illusion only). + +actions in controlled.npz are normalized [-1,+1]. Physical omega = (action*scale+bias)*U0. + +## Per-Scene Notes + +- **Karman**: 7 objects, obs_slice=(2,14), action_bias=[0,-4,4], s_dim=12 +- **Illusion**: 6 objects, obs_slice=(0,12), action_bias=[0,-2,2], s_dim=14, FIFO bias=[0,-U0,U0] differs from DRL bias +- **Vortex**: 6 objects, MAX_STEPS=150, action_scale=4, vortex added after DDF checkpoint + +## Expected Similarities + +Karman re100 ~0.90, Illusion 1L ~0.97, Illusion 0.75L ~0.98, Illusion 1.5L ~0.95 diff --git a/src/SR_analysis/STAGE_2_FIT.md b/src/SR_analysis/STAGE_2_FIT.md new file mode 100644 index 0000000..e7457a5 --- /dev/null +++ b/src/SR_analysis/STAGE_2_FIT.md @@ -0,0 +1,45 @@ +# Stage 2: PySR Symbolic Regression + +Fits interpretable formulas (obs → act) from controlled.npz data using PySR. + +## Usage + +```bash +# Per-scene fitting +conda run -n sr_env python stage_2_fit.py --scene karman_re100 --mode per-scene + +# Joint cross-scene +conda run -n sr_env python stage_2_fit.py --scenes karman_re50,karman_re100,karman_re200,karman_re400 --mode joint + +# Deep search +conda run -n sr_env python stage_2_fit.py --scenes illusion_0.75L,illusion_1L --mode joint --deep +``` + +## Output + +`results/formulas/{label}_{front,top}.json`: best_sympy formula, feature_keys, R2 score. + +Fitting target is non-dimensional **alpha = omega/U0** (not physical omega). + +## Feature Sets + +- **PHASE_STATE_KEYS** (6): u_a, du_a_dt, Cl_tot, dCl_tot_dt, Cd_tot, Cd_rear — Karman per-Re +- **ILLUSION_PHASE_KEYS** (10): above + Cd_err, Cl_err, dCd_err_dt, dCl_err_dt — Illusion +- **PHYS_DADT + mu** (17): physics + daF/dt + mu modulation — Karman joint + +## Per-Scene vs Joint + +- **per-scene**: Fit one formula per scene. Use for individual Re/diameter analysis. +- **joint**: Concatenate multiple scenes' data, fit single formula. Use for cross-scene generalization. + +## Formula Review Checklist + +After fitting, check: +1. **No spurious terms**: `daB_dt` = 0 at deployment (rear constant), remove if present +2. **Front no-bias**: α_F ≈ 0 when all features ≈ 0 +3. **Rear shared-head**: α_B = -α_T when flow is symmetric (G-mirror applied) +4. **One-step R2 ≠ closed-loop**: Always validate in Stage 3 + +## Environments + +`conda run -n sr_env` (PySR installed, separate from pycuda to avoid CUDA conflicts). diff --git a/src/SR_analysis/STAGE_3_VALIDATE.md b/src/SR_analysis/STAGE_3_VALIDATE.md new file mode 100644 index 0000000..97e488e --- /dev/null +++ b/src/SR_analysis/STAGE_3_VALIDATE.md @@ -0,0 +1,52 @@ +# Stage 3: CFD Closed-Loop Validation + +Validates PySR formulas or PPO baselines in closed-loop CFD. Final judge is DTW similarity (not one-step R2). + +## Usage + +```bash +# PySR formula validation +conda run -n pycuda_3_10 python stage_3_validate.py \ + --scene karman_re100 --device 2 --mode pysr \ + --formula-front results/formulas/karman_joint_front.json \ + --formula-top results/formulas/karman_joint_top.json + +# PPO baseline +conda run -n pycuda_3_10 python stage_3_validate.py --scene illusion_1L --device 2 --mode ppo + +# Batch generalization +conda run -n pycuda_3_10 python stage_3_validate.py \ + --group illusion_generalization --device 2 --mode pysr \ + --formula-front results/formulas/illusion_joint_front.json \ + --formula-top results/formulas/illusion_joint_top.json +``` + +## Modes + +- **pysr**: Deploy PySR formula in closed-loop CFD (v23: front no-bias, rear shared-head) +- **ppo**: Run trained PPO model as baseline +- **uncontrolled**: Zero-action baseline + +## Steps + +Auto-calculated from sample interval: SI=400→320, SI=600→214, SI=800→160. + +## Output + +`results/validations/{scene_name}.json`: similarity score, mode, n_steps. + +## v23 Structure + +- Front: α_F = f_front(x), no bias — should be ~0 when features ~0 +- Top: α_T = f_rear(x), with bias +- Bottom: α_B = -f_rear(G[x]) — shared-head via G-mirror + +G-mirror: [aF,aT,aB]→[-aF,-aB,-aT], sensors swap top↔bottom with v sign flip. + +## Similarity Interpretation + +- > 0.90: Excellent (cloak/illusion effective) +- 0.70-0.90: Partial control +- < 0.50: Poor + +Tail similarity isolates the far-wake portion. diff --git a/src/SR_analysis/STAGE_4_ANALYZE.md b/src/SR_analysis/STAGE_4_ANALYZE.md new file mode 100644 index 0000000..4028f62 --- /dev/null +++ b/src/SR_analysis/STAGE_4_ANALYZE.md @@ -0,0 +1,38 @@ +# Stage 4: Analysis & Visualization + +Analyzes PPO policies and SR formulas. Generates FFT spectra, action timeseries, and degradation curves. + +## Usage + +```bash +# PPO action visualization (timeseries + FFT) +conda run -n pycuda_3_10 python stage_4_analyze.py --scene illusion_1L --mode ppo-viz + +# Cross-diameter degradation analysis +conda run -n pycuda_3_10 python stage_4_analyze.py \ + --scenes illusion_0.75L,illusion_1L,illusion_1.5L --mode degradation + +# Formula comparison (coming soon) +conda run -n pycuda_3_10 python stage_4_analyze.py --scene illusion_1L --mode formula-compare +``` + +## Modes + +- **ppo-viz**: Action timeseries plot + FFT spectrum for each cylinder +- **degradation**: Cross-scene comparison (alpha std, Cd_tot, action range) — finds transition points where control regime changes +- **formula-compare**: (placeholder) Compare PySR formula predictions vs PPO actions + +## Output + +Figures written to `data/figures/`: +- `ppo_viz_{scene}.png` — timeseries + FFT +- `degradation_metrics.png` — cross-diameter comparison panels + +## Regime Detection + +The degradation mode detects control regime transitions by: +1. Action amplitude jump (alpha std > 4x) +2. Frequency shift (FFT dominant frequency > 5x) +3. Autocorrelation pattern change (lag-2 ≈ -0.9 = high-frequency switching) + +Known transition: Illusion 1.5L shifts from phase-lead compensation to high-frequency periodic modulation. diff --git a/src/SR_analysis/core/cfd.py b/src/SR_analysis/core/cfd.py new file mode 100644 index 0000000..a0b0f5e --- /dev/null +++ b/src/SR_analysis/core/cfd.py @@ -0,0 +1,403 @@ +"""CFD interface for LegacyCelerisLab (pycuda_3_10 env). + +All functions use the LegacyCelerisLab (old) CFD API via: + from LegacyCelerisLab import FlowField + +Must be run inside: conda run -n pycuda_3_10 + +NOTE: This module should be imported directly, not through SR_analysis.utils +because it requires pycuda. Other utils (sindy_fitter, feature_builder, g_operator) +do NOT require pycuda and can be imported from the __init__. +""" +from __future__ import annotations + +import json +import os +import sys +from collections import deque +from typing import Any, Dict, List, Optional, Tuple + +import numpy as np + +# -- Import legacy CFD ------------------------------------------------------- +# LegacyCelerisLab lives at the repo root; SR_analysis is at repo_root/src/SR_analysis. +_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) +if _REPO not in sys.path: + sys.path.insert(0, _REPO) +_SRC = os.path.join(_REPO, "src") +if _SRC not in sys.path: + sys.path.insert(0, _SRC) + +from LegacyCelerisLab import FlowField # noqa: E402 +from LegacyCelerisLab import utils as legacy_utils # noqa: E402 + +# --------------------------------------------------------------------------- +# Action-smoothing constant (legacy run() internal) +# --------------------------------------------------------------------------- +ACTION_SMOOTH_WEIGHT = 0.1 # used by FlowField.run() internally + + +def nu_from_re(re_code: float, u0: float = 0.01, d_ref: float = 40.0) -> float: + """Return kinematic viscosity for a given code Reynolds number. + + ``re_code`` uses reference length *2*D* = 40.0 (matching model file naming). + """ + return u0 * d_ref / re_code + + +def load_legacy_configs(config_dir: str) -> Tuple[Any, Any]: + """Load and return legacy (cuda_config, field_config) from *config_dir*.""" + cuda_cfg = legacy_utils.load_cuda_config( + os.path.join(config_dir, "config_cuda.json") + ) + field_cfg = legacy_utils.load_flow_field_config( + os.path.join(config_dir, "config_flowfield.json") + ) + return cuda_cfg, field_cfg + + +# --------------------------------------------------------------------------- +# Environment helpers -- Karman cloak (disturbance cylinder + pinball) +# --------------------------------------------------------------------------- + +def build_karman_cloak_env( + flow_field: FlowField, + *, + u0: float, + l0: float, + sample_interval: int, + fifo_len: int, + data_type: type, +) -> Tuple[np.ndarray, dict]: + """Phase 0-1: add dist-cylinder & 3 sensors, stabilize, record target. + + Steps (mirrors env_karman_cloak_standard.__init__): + 1. add dist_cylinder (id=0) + 2. add 3 sensors (id=1,2,3) + 3. stabilize run(4*NX/U0, zero-action[4]) + 4. record FIFO_LEN x run(SAMPLE_INTERVAL, zero[4]), collect obs[2:8] + + Returns + ------- + target_states : ndarray (FIFO_LEN, 6) -- sensor0/1/2 ux,uy + info : dict with n_objects, NX, NY + """ + # dist cylinder + center = (10.0 * l0, (flow_field.FIELD_SHAPE[1] - 1) / 2, 0.0) + flow_field.add_cylinder(center, l0) + + # sensors + for y_off in [2.0, 0.0, -2.0]: + sc = (40.0 * l0, (flow_field.FIELD_SHAPE[1] - 1) / 2 + y_off * l0, 0.0) + flow_field.add_sensor(sc, l0 / 4.0) + + n_obj = flow_field.obs.size // 2 + + # stabilize + stabilize_steps = int(4 * flow_field.FIELD_SHAPE[0] / u0) + print(f" stabilising ({stabilize_steps} steps)...") + flow_field.run(stabilize_steps, np.zeros(n_obj, dtype=data_type)) + + # record target (only sensor signals = obs[2:8]) + target_states = np.empty((0, 6), dtype=data_type) + for _ in range(fifo_len): + flow_field.run(sample_interval, np.zeros(n_obj, dtype=data_type)) + new_state = flow_field.obs.copy()[2:8] + target_states = np.vstack((target_states, new_state)) + + print(f" target recorded: {target_states.shape}") + return target_states, {"n_objects": n_obj, "NX": flow_field.FIELD_SHAPE[0], + "NY": flow_field.FIELD_SHAPE[1]} + + +def add_pinball( + flow_field: FlowField, + *, + l0: float, + u0: float, + sample_interval: int, + fifo_len: int, + data_type: type, + action_bias: Optional[Tuple[float, float, float]] = None, + pinball_front_x: float = 30.0, + pinball_rear_x: float = 31.3, + obs_slice_start: int = 2, + obs_slice_end: int = 14, + n_objects_total: Optional[int] = None, +) -> dict: + """Add pinball cylinders, stabilize, compute norm, bias rollout. + + Steps: + 1. add front, bottom, top cylinders + 2. stabilize run(4*NX/U0, zero-action) + 3. get_ddf() + save_ddf() (checkpoint) + 4. FIFO_LEN x run(SAMPLE_INTERVAL, zero) -> compute norm + 5. apply_ddf() (restore pre-bias state) + 6. FIFO_LEN x run(SAMPLE_INTERVAL, bias-action) -> save_states + 7. apply_ddf() + + Parameters + ---------- + pinball_front_x, pinball_rear_x : pinball geometry (L0 units). + Default 30.0/31.3 for Karman; 19.0/20.3 for Illusion. + obs_slice_start, obs_slice_end : slice of obs for norm. + Default [2:14] for Karman (7 objects); [0:12] for Illusion (6 objects). + n_objects_total : if provided, used for bias array length. + Default: inferred from flow_field after adding cylinders. + + Returns dict with norm values. + """ + if action_bias is None: + action_bias = (0.0, -4.0, 4.0) + + u0_float = float(u0) + + # add 3 pinball cylinders + ny = flow_field.FIELD_SHAPE[1] + centers = [ + (pinball_front_x * l0, (ny - 1) / 2, 0.0), + (pinball_rear_x * l0, (ny - 1) / 2 + 0.75 * l0, 0.0), + (pinball_rear_x * l0, (ny - 1) / 2 - 0.75 * l0, 0.0), + ] + for c in centers: + flow_field.add_cylinder(c, l0 / 2.0) + + n_obj = flow_field.obs.size // 2 if n_objects_total is None else n_objects_total + print(f" bodies after pinball: {n_obj}") + + # stabilize + stabilize_steps = int(4 * flow_field.FIELD_SHAPE[0] / u0_float) + print(f" stabilising pinball ({stabilize_steps} steps)...") + flow_field.run(stabilize_steps, np.zeros(n_obj, dtype=data_type)) + + # checkpoint DDF + flow_field.get_ddf() + flow_field.save_ddf() + + # --- norm phase (zero-action) --- + fifo = deque(maxlen=fifo_len) + for _ in range(fifo_len): + flow_field.run(sample_interval, np.zeros(n_obj, dtype=data_type)) + fifo.append(flow_field.obs.copy()[obs_slice_start:obs_slice_end]) + + temp_states = np.array(fifo, dtype=data_type) + # forces are at indices [6:12] relative to the slice end + force_start = obs_slice_end - obs_slice_start - 6 + force_end = force_start + 6 + force_norm_fact = 6.0 * float(np.max(np.abs(temp_states[:, force_start:force_end]))) + sens_deviation = np.mean(temp_states[:, 0:6], axis=0).astype(data_type) + sens_norm_fact = np.zeros(6, dtype=data_type) + for i in range(6): + sens_norm_fact[i] = 5.0 * float(np.max(np.abs(temp_states[:, i] - sens_deviation[i]))) + + print(f" norm: force_norm_fact={force_norm_fact:.6f}") + print(f" norm: sens_deviation={sens_deviation}") + print(f" norm: sens_norm_fact={sens_norm_fact}") + + # --- bias-action rollout --- + flow_field.apply_ddf() + bias = np.zeros(n_obj, dtype=data_type) + bias[n_obj - 3] = float(action_bias[0] * u0_float) + bias[n_obj - 2] = float(action_bias[1] * u0_float) + bias[n_obj - 1] = float(action_bias[2] * u0_float) + print(f" bias action: {bias}") + + fifo.clear() + for _ in range(fifo_len): + flow_field.run(sample_interval, bias) + fifo.append(flow_field.obs.copy()[obs_slice_start:obs_slice_end]) + + save_states = np.array(list(fifo), dtype=data_type) + # CRITICAL: save DDF again AFTER bias FIFO, so restore_ddf() goes + # to the post-bias state (consistent with saved FIFO). + # Without this, reset() restores to a bare stabilized state with + # no bias history, invalidating the FIFO. + flow_field.get_ddf() + flow_field.save_ddf() + flow_field.apply_ddf() + + return { + "force_norm_fact": force_norm_fact, + "sens_deviation": sens_deviation.tolist(), + "sens_norm_fact": sens_norm_fact.tolist(), + "action_bias": list(action_bias), + "save_states": save_states, + } + + +def build_observation( + obs_slice: np.ndarray, + norm: dict, +) -> np.ndarray: + """Assemble normalised DRL observation (12-dim) from a single obs slice. + + ``obs_slice`` is 12-element: sensor[0:6] + force[6:12]. + + Returns clipped 12-dim array in [-1, 1]. + """ + forces = obs_slice[6:12] / norm["force_norm_fact"] + sens = (obs_slice[0:6] - norm["sens_deviation"]) / norm["sens_norm_fact"] + obs = np.clip(np.hstack([forces, sens]), -1.0, 1.0).astype(np.float32) + return obs + + +def action_to_physical( + action_norm: np.ndarray, + *, + scale: float = 8.0, + bias: Tuple[float, float, float] = (0.0, -4.0, 4.0), + u0: float = 0.01, +) -> np.ndarray: + """Convert normalized action [-1,1] to physical omega (lattice units). + + physical_omega[i] = (action_norm[i] * scale + bias[i]) * u0 + """ + action_norm = np.asarray(action_norm, dtype=np.float64).reshape(-1, 3) + bias_arr = np.array(bias, dtype=np.float64) + return (action_norm * scale + bias_arr) * u0 + + +def scale_action( + action_norm: np.ndarray, + *, + scale: float = 8.0, + bias: Tuple[float, float, float] = (0.0, -4.0, 4.0), + u0: float = 0.01, + n_total_bodies: int = 7, +) -> np.ndarray: + """Convert normalised action ([-1,1]^3) to legacy CFD action array. + + Returns array of length *n_total_bodies* with cylinders' omegas at the + last 3 slots. + """ + a = np.zeros(n_total_bodies, dtype=np.float32) + omega = (np.array(action_norm, dtype=np.float32) * scale + np.array(bias, dtype=np.float32)) * u0 + a[n_total_bodies - 3:] = omega + return a + + +# --------------------------------------------------------------------------- +# Vorticity & field export +# --------------------------------------------------------------------------- + +def vorticity_from_ddf(flow_field: FlowField, u0: float) -> np.ndarray: + """Compute z-vorticity from current DDF on host.""" + flow_field.get_ddf() + ddf = flow_field.ddf.copy().reshape((9, flow_field.FIELD_SHAPE[1], + flow_field.FIELD_SHAPE[0])).transpose(2, 1, 0) + ux = (ddf[:, :, 1] + ddf[:, :, 5] + ddf[:, :, 8] + - ddf[:, :, 3] - ddf[:, :, 6] - ddf[:, :, 7]) / u0 + uy = (ddf[:, :, 2] + ddf[:, :, 5] + ddf[:, :, 6] + - ddf[:, :, 4] - ddf[:, :, 7] - ddf[:, :, 8]) / u0 + omega = np.gradient(uy, axis=0) - np.gradient(ux, axis=1) + return omega.astype(np.float64) + + +def save_vorticity_png(path: str, omega: np.ndarray, title: str = ""): + """Save vorticity field as a PNG with symmetric colour bar.""" + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + abs_o = np.abs(omega[np.isfinite(omega)]) + vmax = float(np.percentile(abs_o, 99.5)) if abs_o.size > 0 else 1.0 + if vmax <= 0: + vmax = 1.0 + + ny, nx = omega.shape + fig, ax = plt.subplots(figsize=(min(18, max(8, nx / 60)), min(10, max(3, ny / 40)))) + im = ax.imshow(omega, origin="lower", aspect="equal", cmap="RdBu_r", + vmin=-vmax, vmax=vmax, extent=(0, nx - 1, 0, ny - 1)) + ax.set_xlabel("x (lattice)") + ax.set_ylabel("y (lattice)") + if title: + ax.set_title(title) + fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04, label=r"$\omega_z$") + fig.tight_layout() + fig.savefig(path, dpi=150, bbox_inches="tight") + plt.close(fig) + + +# --------------------------------------------------------------------------- +# DTW similarity +# --------------------------------------------------------------------------- + +def calc_lag(target: np.ndarray, state: np.ndarray) -> int: + """Find lag that maximises cross-correlation between two 1-D signals.""" + t = target - np.mean(target) + s = state - np.mean(state) + corr = np.correlate(t, s, mode="full") + lags = np.arange(-len(target) + 1, len(target)) + return int(lags[np.argmax(corr)]) + + +def calc_dtw_sim(target: np.ndarray, state: np.ndarray) -> float: + """DTW-based similarity: 1 - (DTW distance / len(target)).""" + n, m = len(target), len(state) + dtw = np.full((n + 1, m + 1), np.inf) + dtw[0, 0] = 0.0 + for i in range(1, n + 1): + for j in range(1, m + 1): + cost = abs(float(target[i - 1]) - float(state[j - 1])) + dtw[i, j] = cost + min(dtw[i - 1, j], dtw[i, j - 1], dtw[i - 1, j - 1]) + return float(1.0 - dtw[n, m] / n) + + +def compute_similarity( + target_states: np.ndarray, + state_series: np.ndarray, + conv_len: int, +) -> float: + """Compute lag-compensated DTW similarity over *conv_len* window.""" + ref = target_states[conv_len:2 * conv_len, 1] + cur = state_series[-conv_len:, 1] + lag = calc_lag(ref, cur) + + sim_sum = 0.0 + for i in range(6): + target_seq = np.roll(target_states[:, i], -lag)[conv_len:2 * conv_len] + state_seq = state_series[-conv_len:, i] + sim_sum += calc_dtw_sim(target_seq, state_seq) / 6.0 + return float(sim_sum) + + +# --------------------------------------------------------------------------- +# Dummy env for loading SB3 models +# --------------------------------------------------------------------------- + +def create_dummy_env(s_dim: int = 12, a_dim: int = 3): + """Return a gym.Env with correct observation/action spaces for model loading.""" + import gymnasium as gym + from gymnasium import spaces + + class DummyEnv(gym.Env): + def __init__(self): + super().__init__() + self.observation_space = spaces.Box(low=-1, high=1, shape=(s_dim,), dtype=np.float32) + self.action_space = spaces.Box(low=-1, high=1, shape=(a_dim,), dtype=np.float32) + + def reset(self, seed=None): + return np.zeros(s_dim, dtype=np.float32), {} + + def step(self, action): + return np.zeros(s_dim, dtype=np.float32), 0.0, False, False, {} + + def render(self): + pass + + return DummyEnv() + + +def load_ppo_model(model_path: str, device: str = "cuda:0", s_dim: int = 12, a_dim: int = 3): + """Load a PPO model with Sin activation.""" + import torch + from torch.nn import Module + from stable_baselines3 import PPO + + class Sin(Module): + def forward(self, x): + return torch.sin(x) + + dummy_env = create_dummy_env(s_dim, a_dim) + model = PPO.load(model_path, env=dummy_env, device=device) + return model diff --git a/src/SR_analysis/core/features.py b/src/SR_analysis/core/features.py new file mode 100644 index 0000000..8713f50 --- /dev/null +++ b/src/SR_analysis/core/features.py @@ -0,0 +1,386 @@ +"""Unified feature builder for all cloak scenes. + +Produces dimensionless features with consistent G-equivariant structure. +All scenes (Karman, steady, vortex, illusion) use this same builder. + +Copy of analysis_cloak/common/feature_builder.py -- kept as canonical source. +""" +from __future__ import annotations + +from typing import Dict, List, Optional, Tuple + +import numpy as np + + +# -- Physical constants ------------------------------------------------------ +U0 = 0.01 # inlet velocity (lattice units) +D_CYL = 20.0 # cylinder diameter (lattice) + + +# -- Dimensionless conversion ------------------------------------------------ + +def compute_dimensionless( + sensors: np.ndarray, # (T, 6) raw lattice [s0_ux,s0_uy, s1_ux,s1_uy, s2_ux,s2_uy] + forces: np.ndarray, # (T, 6) raw lattice [f0_fx,f0_fy, f1_fx,f1_fy, f2_fx,f2_fy] + u0: float = U0, + d: float = D_CYL, + rho: float = 1.0, +) -> Dict[str, np.ndarray]: + """Convert raw lattice CFD data to dimensionless quantities. + + Sensor order: [s0_ux,s0_uy, s1_ux,s1_uy, s2_ux,s2_uy] + where s0=top(y=+2L0), s1=mid(y=0), s2=bottom(y=-2L0) + Force order: [front_fx,front_fy, bottom_fx,bottom_fy, top_fx,top_fy] + + Returns: + u_hat_B, u_hat_C, u_hat_T: nondim streamwise velocity (bottom/centre/top) + v_hat_B, v_hat_C, v_hat_T: nondim crosswise velocity + Cd_F, Cd_T, Cd_B: drag coefficient per cylinder + Cl_F, Cl_T, Cl_B: lift coefficient per cylinder + """ + s = np.asarray(sensors, dtype=np.float64) + f = np.asarray(forces, dtype=np.float64) + + # Sensor positions: s0=top, s1=centre, s2=bottom + # Convention: B=bottom=s2, C=centre=s1, T=top=s0 + return { + "u_hat_T": s[:, 0] / u0, + "v_hat_T": s[:, 1] / u0, + "u_hat_C": s[:, 2] / u0, + "v_hat_C": s[:, 3] / u0, + "u_hat_B": s[:, 4] / u0, + "v_hat_B": s[:, 5] / u0, + "Cd_F": 2.0 * f[:, 0] / (rho * u0**2 * d), + "Cl_F": 2.0 * f[:, 1] / (rho * u0**2 * d), + "Cd_B": 2.0 * f[:, 2] / (rho * u0**2 * d), + "Cl_B": 2.0 * f[:, 3] / (rho * u0**2 * d), + "Cd_T": 2.0 * f[:, 4] / (rho * u0**2 * d), + "Cl_T": 2.0 * f[:, 5] / (rho * u0**2 * d), + } + + +# -- G operator (corrected) -------------------------------------------------- + +def apply_G_alpha(alpha: np.ndarray) -> np.ndarray: + """Apply mirror G to action: [aF, aT, aB] -> [-aF, -aB, -aT].""" + return np.array([-alpha[0], -alpha[2], -alpha[1]], dtype=alpha.dtype) + + +def apply_G_x(dim: Dict[str, np.ndarray], + a_prev: np.ndarray, + a_prev2: np.ndarray) -> Tuple[Dict, np.ndarray, np.ndarray]: + """Apply G to dimensionless state. + + Returns (G_dim, G_a_prev, G_a_prev2) with corrected sign rules. + """ + G_dim = { + "u_hat_B": dim["u_hat_T"], "u_hat_C": dim["u_hat_C"], "u_hat_T": dim["u_hat_B"], + "v_hat_B": -dim["v_hat_T"], "v_hat_C": -dim["v_hat_C"], "v_hat_T": -dim["v_hat_B"], + "Cd_F": dim["Cd_F"], "Cd_T": dim["Cd_B"], "Cd_B": dim["Cd_T"], + "Cl_F": -dim["Cl_F"], "Cl_T": -dim["Cl_B"], "Cl_B": -dim["Cl_T"], + } + G_a_prev = np.column_stack([-a_prev[:, 0], -a_prev[:, 2], -a_prev[:, 1]]) + G_a_prev2 = np.column_stack([-a_prev2[:, 0], -a_prev2[:, 2], -a_prev2[:, 1]]) + return G_dim, G_a_prev, G_a_prev2 + + +# -- Feature key definitions ------------------------------------------------- + +# Original feature set (includes sin_ua/cos_ua) +CORE_FEAT_KEYS = [ + "u_m", "u_a", "u_c", + "v_a", + "Cd_tot", "Cd_rear", + "Cl_tot", "Cl_diff", + "sin_ua", "cos_ua", + "aF_lag1", "aB_lag1", "aT_lag1", + "daF", "daB", "daT", +] + +# V2 core features: no sin_ua/cos_ua, no mu (for single-scene fitting) +CORE_FEAT_KEYS_V2 = [ + "u_m", "u_a", "u_c", + "v_a", + "Cd_tot", "Cd_rear", + "Cl_tot", "Cl_diff", + "aF_lag1", "aB_lag1", "aT_lag1", + "daF", "daB", "daT", +] + +# Time-explicit features: da/dt_c (for cross-scene coefficient comparison) +TIME_FEAT_KEYS = [ + "daF_dt", "daB_dt", "daT_dt", +] + +MU_FEAT_KEYS = ["mu", "mu_u_a", "mu_v_a", "mu_Cd_tot", "mu_Cl_diff"] + +# Illusion target force features (for scenes where we know the target Cd/Cl) +ILLUSION_TARGET_KEYS = ["target_Cd", "target_Cl"] +ILLUSION_FEAT_KEYS_V2 = CORE_FEAT_KEYS_V2 + ILLUSION_TARGET_KEYS +ILLUSION_ALL_FEAT_KEYS_V2 = ILLUSION_FEAT_KEYS_V2 + MU_FEAT_KEYS # with mu for joint + +# Physics-only feature keys: NO action history terms (no aF_lag1, no daF, etc.) +# These are the clean inputs for learning d(alpha)/dt as a function of physics state. +PHYSICS_FEAT_KEYS = [ + "u_m", "u_a", "u_c", "v_a", + "Cd_tot", "Cd_rear", "Cl_tot", "Cl_diff", +] + +# Illusion error-state: physics features + force error (not raw target forces) +ILLUSION_ERR_KEYS = PHYSICS_FEAT_KEYS + ["Cd_err", "Cl_err"] + +# Observation lag (1-step delayed) and derivative (time-normalized) features +# These add temporal/phasing information to the otherwise static physics features. +LAG_FEAT_KEYS = [ + "u_m_lag1", "u_a_lag1", "u_c_lag1", "v_a_lag1", + "Cd_tot_lag1", "Cd_rear_lag1", "Cl_tot_lag1", "Cl_diff_lag1", +] +DERIV_FEAT_KEYS = [ + "du_m_dt", "du_a_dt", "du_c_dt", "dv_a_dt", + "dCd_tot_dt", "dCd_rear_dt", "dCl_tot_dt", "dCl_diff_dt", +] +# Action lag (for ablation level 4 — comparing against old approach) +ACTION_LAG_KEYS = ["aF_lag1", "aB_lag1", "aT_lag1"] + +# Augmented levels for ablation study: +# Level 0 = x_n (PHYSICS_FEAT_KEYS, no memory at all) +# Level 1 = x_n + x_{n-1} (current + 1-step lag) +# Level 2 = x_n + dx/dt (current + derivative) +# Level 3 = x_n + x_{n-1} + dx/dt (full temporal context) +# Level 4 = Level 3 + a_{n-1} (add action history for comparison) +AUG_LEVEL_1_KEYS = PHYSICS_FEAT_KEYS + LAG_FEAT_KEYS +AUG_LEVEL_2_KEYS = PHYSICS_FEAT_KEYS + DERIV_FEAT_KEYS +AUG_LEVEL_3_KEYS = PHYSICS_FEAT_KEYS + LAG_FEAT_KEYS + DERIV_FEAT_KEYS +AUG_LEVEL_4_KEYS = AUG_LEVEL_3_KEYS + ACTION_LAG_KEYS + +# Illusion equivalents with error-state +ILLUSION_LAG_KEYS = [ + "Cd_err_lag1", "Cl_err_lag1", +] +ILLUSION_DERIV_KEYS = [ + "dCd_err_dt", "dCl_err_dt", +] +ILLUSION_AUG_LEVEL_1_KEYS = ILLUSION_ERR_KEYS + LAG_FEAT_KEYS + ILLUSION_LAG_KEYS +ILLUSION_AUG_LEVEL_2_KEYS = ILLUSION_ERR_KEYS + DERIV_FEAT_KEYS + ILLUSION_DERIV_KEYS +ILLUSION_AUG_LEVEL_3_KEYS = ILLUSION_AUG_LEVEL_1_KEYS + DERIV_FEAT_KEYS + ILLUSION_DERIV_KEYS +ILLUSION_AUG_LEVEL_4_KEYS = ILLUSION_AUG_LEVEL_3_KEYS + ACTION_LAG_KEYS + +# Phase-state features: low-dimensional dynamic state z = [phase_obs, phase_deriv, static_force] +# This replaces the full x_n + x_{n-1} with a compact representation. +# The idea: u_a + du_a/dt encodes oscillation phase, Cl_tot + dCl_tot/dt encodes lift dynamics, +# and Cd_tot/Cd_rear provide static force feedback. +PHASE_STATE_KEYS = [ + "u_a", "du_a_dt", # oscillation phase (cross-stream asymmetry + rate) + "Cl_tot", "dCl_tot_dt", # lift dynamics + "Cd_tot", "Cd_rear", # static drag feedback +] + +# Karman expanded: phase-state + supplementary static quantities +# For Karman, 6-dim phase-state may not capture enough information. +# Adding u_m (mean streamwise), u_c (center sensor), v_a (cross asymmetry), Cl_diff (lift distribution) +KARMAN_EXPANDED_KEYS = PHASE_STATE_KEYS + [ + "u_m", "u_c", "v_a", "Cl_diff", +] + +# Illusion phase-state: error-based + dynamics +ILLUSION_PHASE_KEYS = PHASE_STATE_KEYS + [ + "Cd_err", "Cl_err", "dCd_err_dt", "dCl_err_dt", +] + +ALL_FEAT_KEYS = CORE_FEAT_KEYS + MU_FEAT_KEYS +# V2 all features (for cross-Re joint fitting with mu) +ALL_FEAT_KEYS_V2 = CORE_FEAT_KEYS_V2 + MU_FEAT_KEYS + + +# -- Feature computation ----------------------------------------------------- + +def compute_features( + dim: Dict[str, np.ndarray], + actions_prev: np.ndarray, # (T, 3) physical omega(t-1) or nondim alpha(t-1) + actions_prev2: np.ndarray, # (T, 3) physical omega(t-2) + mu: float, + alpha_mode: bool = False, # if True, actions_prev are already nondim alpha + include_mu: bool = True, + include_cos_sin: bool = True, # if True, include sin_ua/cos_ua features + dt_c: float = 1.0, # control interval in T0 units (for time normalization) + u0: float = U0, # inlet velocity for omega->alpha conversion + target_forces: Optional[np.ndarray] = None, # (T, 2) raw lattice target forces [fx,fy] + rho: float = 1.0, # fluid density for Cd/Cl conversion + sensors_raw: Optional[np.ndarray] = None, # (T, 6) raw lattice sensors, for obs dynamics + forces_raw: Optional[np.ndarray] = None, # (T, 6) raw lattice forces, for obs dynamics +) -> Dict[str, np.ndarray]: + """Compute unified feature dictionary from dimensionless primitives. + + Args: + dim: from compute_dimensionless() + actions_prev: lagged actions (physical omega or nondim alpha) + actions_prev2: twice-lagged actions + mu: 1/Re_D + alpha_mode: if True, actions are already nondim; else convert + include_mu: include mu modulation terms + include_cos_sin: include sin_ua/cos_ua phase encoding + dt_c: control interval (in T0 = D/U0 units), for time-normalized deltas + u0: inlet velocity (lattice), used only when alpha_mode=False + + Returns dict with all features as (T,) or (T,3) arrays. + """ + T = actions_prev.shape[0] + u_B, u_C, u_T = dim["u_hat_B"], dim["u_hat_C"], dim["u_hat_T"] + v_B, v_C, v_T = dim["v_hat_B"], dim["v_hat_C"], dim["v_hat_T"] + Cd_F, Cd_T, Cd_B = dim["Cd_F"], dim["Cd_T"], dim["Cd_B"] + Cl_F, Cl_T, Cl_B = dim["Cl_F"], dim["Cl_T"], dim["Cl_B"] + + # If actions are in physical omega, convert to nondim alpha + if alpha_mode: + a = actions_prev.astype(np.float64) + a2 = actions_prev2.astype(np.float64) + else: + a = actions_prev.astype(np.float64) / u0 + a2 = actions_prev2.astype(np.float64) / u0 + + sym = {} + + # Sensor combinations (nondim) + sym["u_m"] = (u_B + u_C + u_T) / 3.0 + sym["u_a"] = (u_T - u_B) / 2.0 + sym["u_c"] = u_C.copy() + sym["v_a"] = (v_T - v_B) / 2.0 + + # Force combinations (dimensionless Cd/Cl) + sym["Cd_tot"] = Cd_F + Cd_T + Cd_B + sym["Cd_rear"] = Cd_T + Cd_B + sym["Cl_tot"] = Cl_F + Cl_T + Cl_B + sym["Cl_diff"] = Cl_T - Cl_B + + # Phase (optional, may obscure linear structure) + if include_cos_sin: + sym["sin_ua"] = np.sin(np.pi * sym["u_a"]) + sym["cos_ua"] = np.cos(np.pi * sym["u_a"]) + + # Memory (nondim alpha) -- discrete version + sym["aF_lag1"] = a[:, 0] + sym["aB_lag1"] = a[:, 1] + sym["aT_lag1"] = a[:, 2] + sym["daF"] = a[:, 0] - a2[:, 0] + sym["daB"] = a[:, 1] - a2[:, 1] + sym["daT"] = a[:, 2] - a2[:, 2] + + # Time-normalized deltas (for cross-scene coefficient comparison) + sym["daF_dt"] = sym["daF"] / dt_c + sym["daB_dt"] = sym["daB"] / dt_c + sym["daT_dt"] = sym["daT"] / dt_c + + # Target forces (for illusion scenes) — convert lattice forces to Cd/Cl + if target_forces is not None: + tf = np.asarray(target_forces, dtype=np.float64) + if tf.ndim == 1: + tf = tf.reshape(1, -1) + sym["target_Cd"] = 2.0 * tf[:, 0] / (rho * u0**2 * D_CYL) + sym["target_Cl"] = 2.0 * tf[:, 1] / (rho * u0**2 * D_CYL) + # Error-state: deviation between actual and target force + sym["Cd_err"] = sym["Cd_tot"] - sym["target_Cd"] + sym["Cl_err"] = sym["Cl_tot"] - sym["target_Cl"] + + # Mu modulation + if include_mu: + sym["mu"] = np.full(T, mu, dtype=np.float64) + sym["mu_u_a"] = sym["u_a"] * mu + sym["mu_v_a"] = sym["v_a"] * mu + sym["mu_Cd_tot"] = sym["Cd_tot"] * mu + sym["mu_Cl_diff"] = sym["Cl_diff"] * mu + sym["mu_Cl_tot"] = sym["Cl_tot"] * mu # additional for phase-state + + # Observation dynamics: 1-step lag + time-normalized derivative + # These add temporal/phase info that static features lack. + if sensors_raw is not None and forces_raw is not None: + sr = np.asarray(sensors_raw, dtype=np.float64) + fr = np.asarray(forces_raw, dtype=np.float64) + # Lag-1 observations (shift by 1, pad first with current) + s_lag1 = np.zeros_like(sr) + f_lag1 = np.zeros_like(fr) + s_lag1[1:] = sr[:-1] + f_lag1[1:] = fr[:-1] + # Compute dim for lag-1 + dim_lag1 = compute_dimensionless(s_lag1, f_lag1, u0=u0, d=D_CYL, rho=rho) + + # Compute combined features for lag-1 + uB_1, uC_1, uT_1 = dim_lag1["u_hat_B"], dim_lag1["u_hat_C"], dim_lag1["u_hat_T"] + vB_1, vC_1, vT_1 = dim_lag1["v_hat_B"], dim_lag1["v_hat_C"], dim_lag1["v_hat_T"] + CdF_1, CdT_1, CdB_1 = dim_lag1["Cd_F"], dim_lag1["Cd_T"], dim_lag1["Cd_B"] + ClF_1, ClT_1, ClB_1 = dim_lag1["Cl_F"], dim_lag1["Cl_T"], dim_lag1["Cl_B"] + + u_m_1 = (uB_1 + uC_1 + uT_1) / 3.0 + u_a_1 = (uT_1 - uB_1) / 2.0 + u_c_1 = uC_1.copy() + v_a_1 = (vT_1 - vB_1) / 2.0 + Cd_tot_1 = CdF_1 + CdT_1 + CdB_1 + Cd_rear_1 = CdT_1 + CdB_1 + Cl_tot_1 = ClF_1 + ClT_1 + ClB_1 + Cl_diff_1 = ClT_1 - ClB_1 + + # Trim to T (in case raw arrays are longer — e.g. validator passes 2 rows) + def _trim(x): + return x[-T:] if x.shape[0] > T else x + + # Store lag-1 features (trimmed to T) + sym["u_m_lag1"] = _trim(u_m_1) + sym["u_a_lag1"] = _trim(u_a_1) + sym["u_c_lag1"] = _trim(u_c_1) + sym["v_a_lag1"] = _trim(v_a_1) + sym["Cd_tot_lag1"] = _trim(Cd_tot_1) + sym["Cd_rear_lag1"] = _trim(Cd_rear_1) + sym["Cl_tot_lag1"] = _trim(Cl_tot_1) + sym["Cl_diff_lag1"] = _trim(Cl_diff_1) + + # Time-normalized derivatives: (current - lag1) / dt_c + eps = 1e-12 + sym["du_m_dt"] = _trim((sym["u_m"] - u_m_1) / (dt_c + eps)) + sym["du_a_dt"] = _trim((sym["u_a"] - u_a_1) / (dt_c + eps)) + sym["du_c_dt"] = _trim((sym["u_c"] - u_c_1) / (dt_c + eps)) + sym["dv_a_dt"] = _trim((sym["v_a"] - v_a_1) / (dt_c + eps)) + sym["dCd_tot_dt"] = _trim((sym["Cd_tot"] - Cd_tot_1) / (dt_c + eps)) + sym["dCd_rear_dt"] = _trim((sym["Cd_rear"] - Cd_rear_1) / (dt_c + eps)) + sym["dCl_tot_dt"] = _trim((sym["Cl_tot"] - Cl_tot_1) / (dt_c + eps)) + sym["dCl_diff_dt"] = _trim((sym["Cl_diff"] - Cl_diff_1) / (dt_c + eps)) + + # Illusion error-state dynamics + if target_forces is not None: + tf = np.asarray(target_forces, dtype=np.float64) + tf_lag1 = np.zeros_like(tf) + tf_lag1[1:] = tf[:-1] + tCd_1 = 2.0 * tf_lag1[:, 0] / (rho * u0**2 * D_CYL) + tCl_1 = 2.0 * tf_lag1[:, 1] / (rho * u0**2 * D_CYL) + sym["Cd_err_lag1"] = _trim(Cd_tot_1 - tCd_1) + sym["Cl_err_lag1"] = _trim(Cl_tot_1 - tCl_1) + sym["dCd_err_dt"] = _trim((sym["Cd_err"] - sym["Cd_err_lag1"]) / (dt_c + eps)) + sym["dCl_err_dt"] = _trim((sym["Cl_err"] - sym["Cl_err_lag1"]) / (dt_c + eps)) + + return sym + + +def build_feature_matrix( + sym: Dict[str, np.ndarray], + feat_keys: List[str], + add_bias: bool = True, +) -> np.ndarray: + """Build feature matrix (T, N) from symbol dict.""" + cols = [] + if add_bias: + cols.append(np.ones(sym[feat_keys[0]].shape[0], dtype=np.float64)) + for k in feat_keys: + if k in sym: + cols.append(np.asarray(sym[k], dtype=np.float64)) + else: + # Missing key -> zero + T = sym.get("u_m", np.ones(1)).shape[0] + cols.append(np.zeros(T, dtype=np.float64)) + return np.column_stack(cols) + + +def get_feature_names(feat_keys: List[str], add_bias: bool = True) -> List[str]: + """Get feature names matching build_feature_matrix output.""" + names = [] + if add_bias: + names.append("bias") + names.extend(feat_keys) + return names diff --git a/src/SR_analysis/core/fitting.py b/src/SR_analysis/core/fitting.py new file mode 100644 index 0000000..db5cc50 --- /dev/null +++ b/src/SR_analysis/core/fitting.py @@ -0,0 +1,503 @@ +"""SINDy fitting utilities: STLSQ threshold grid, feature matrix building. + +All features are built using the unified feature_builder module. +""" +from __future__ import annotations + +from typing import Dict, List, Optional, Tuple + +import numpy as np + +from .feature_builder import ( + compute_dimensionless, compute_features, build_feature_matrix, + get_feature_names, ALL_FEAT_KEYS, U0, +) + +# Default thresholds used across all scenes +DEFAULT_THRESHOLDS = [0.0, 0.001, 0.002, 0.005, 0.01, 0.015, 0.02, 0.03, 0.05, 0.1] + + +def fit_channel( + Theta: np.ndarray, + y: np.ndarray, + thresholds: Optional[List[float]] = None, + alpha: float = 1e-4, + max_iter: int = 25, +) -> Tuple[List[dict], dict]: + """Fit a single channel (one cylinder) with STLSQ threshold grid. + + Returns + ------- + rows : list of dict per threshold + best : dict with best threshold entry (highest R2) + """ + import pysindy as ps + + if thresholds is None: + thresholds = DEFAULT_THRESHOLDS + + # Normalise features for thresholding stability + std = np.std(Theta, axis=0) + std = np.where(std < 1e-8, 1.0, std) + Theta_s = Theta / std + + best = None + rows = [] + for th in thresholds: + opt = ps.STLSQ(threshold=th, alpha=alpha, max_iter=max_iter) + opt.fit(Theta_s, y) + coef = np.asarray(opt.coef_, dtype=np.float64).flatten() / std + y_pred = Theta @ coef + ssr = float(np.sum((y - y_pred) ** 2)) + sst = float(np.sum((y - np.mean(y)) ** 2) + 1e-12) + r2 = 1.0 - ssr / sst + mae = float(np.mean(np.abs(y - y_pred))) + nz = int(np.sum(np.abs(coef) > 1e-8)) + entry = {"threshold": float(th), "nz": nz, "r2": r2, "mae": mae, "coef": coef} + rows.append(entry) + if best is None or r2 > best["r2"]: + best = entry + return rows, best + + +def fit_sindy( + Theta: np.ndarray, + y: np.ndarray, + thresholds: Optional[List[float]] = None, +) -> List[dict]: + """Run SINDy with threshold grid, return results list. + + Each result dict has keys: threshold, nz, r2, mae, coef. + """ + if thresholds is None: + thresholds = DEFAULT_THRESHOLDS + + std = np.std(Theta, axis=0) + std = np.where(std < 1e-8, 1.0, std) + Theta_s = Theta / std + + results = [] + for th in thresholds: + import pysindy as ps + opt = ps.STLSQ(threshold=th, alpha=1e-4, max_iter=25) + opt.fit(Theta_s, y) + coef = np.asarray(opt.coef_, dtype=np.float64).flatten() / std + + y_pred = Theta @ coef + ssr = float(np.sum((y - y_pred) ** 2)) + sst = float(np.sum((y - np.mean(y)) ** 2) + 1e-12) + r2 = 1.0 - ssr / sst + mae = float(np.mean(np.abs(y - y_pred))) + nz = int(np.sum(np.abs(coef) > 1e-8)) + + results.append({ + "threshold": float(th), "nz": nz, "r2": r2, + "mae": mae, "coef": [float(c) for c in coef], + }) + + return results + + +def print_control_law(feature_names: List[str], coef: np.ndarray, channel_label: str = "ch"): + """Pretty-print a sparse control law.""" + terms = [] + for i, c in enumerate(coef): + if abs(c) > 1e-8: + terms.append(f"{c:.6f} * {feature_names[i]}") + print(f" {channel_label}: {' + '.join(terms)}") + nz = sum(1 for c in coef if abs(c) > 1e-8) + print(f" non-zero terms: {nz}") + + +def get_active_support( + coef: np.ndarray, + feat_names: List[str], + relative_threshold: float = 0.02, +) -> Dict[str, float]: + """Extract active features from coefficient vector. + + Features with |coef| / max(|coef|) >= relative_threshold are considered active. + """ + max_c = np.max(np.abs(coef)) + if max_c < 1e-12: + return {} + active = {} + for name, c in zip(feat_names, coef): + if abs(c) / max_c >= relative_threshold: + active[name] = float(c) + return active + + +def get_feature_matrix_from_data( + sensors: np.ndarray, # (T, 6) + forces: np.ndarray, # (T, 6) + actions_phys: np.ndarray, # (T, 3) physical omega + mu: float, + u0: float = U0, + alpha_mode: bool = False, + include_mu: bool = True, + n_warmup: int = 2, +) -> Tuple[np.ndarray, np.ndarray, np.ndarray, List[str], List[str]]: + """Build feature matrices from raw CFD data. + + Constructs dimensionless features via feature_builder, creates front (no bias) + and rear (with bias) feature matrices, and returns them aligned with Y. + + Parameters + ---------- + sensors, forces, actions_phys : raw data arrays. + mu : 1/Re_D. + u0 : inlet velocity (lattice units). + alpha_mode : if True, actions_phys are already nondim alpha. + include_mu : include mu modulation features. + n_warmup : number of warmup steps to discard (default 2 for lag/da). + + Returns + ------- + Theta_front : (T-warmup, N_front) feature matrix, NO bias column + Theta_rear : (T-warmup, N_rear) feature matrix, WITH bias column + Y : (T-warmup, 3) target action matrix + feat_names_front : list of feature names for front + feat_names_rear : list of feature names for rear + """ + T = sensors.shape[0] + a_prev = np.zeros((T, 3), dtype=np.float64) + a_prev2 = np.zeros((T, 3), dtype=np.float64) + a_prev[1:] = actions_phys[:-1] + a_prev2[2:] = actions_phys[:-2] + + dim = compute_dimensionless(sensors, forces, u0=u0, d=20.0) + sym = compute_features(dim, a_prev, a_prev2, mu, + alpha_mode=alpha_mode, include_mu=include_mu, u0=u0) + + Theta_f = build_feature_matrix(sym, ALL_FEAT_KEYS, add_bias=False) + Theta_r = build_feature_matrix(sym, ALL_FEAT_KEYS, add_bias=True) + + feat_names_front = get_feature_names(ALL_FEAT_KEYS, add_bias=False) + feat_names_rear = get_feature_names(ALL_FEAT_KEYS, add_bias=True) + + return (Theta_f[n_warmup:], Theta_r[n_warmup:], + actions_phys[n_warmup:], + feat_names_front, feat_names_rear) + + +# --------------------------------------------------------------------------- +# Weighted STLSQ with Huber-like robust regression +# --------------------------------------------------------------------------- + +def _huber_weights(residuals: np.ndarray, c: float = 1.345) -> np.ndarray: + """Compute Huber-like weights from residuals. + + Args: + residuals: (T,) residual array. + c: tuning constant (default 1.345 gives 95% efficiency for Normal errors). + + Returns: + weights: (T,) weight array in [0, 1]. + """ + s = np.median(np.abs(residuals)) * 1.4826 # robust scale estimate (MAD) + if s < 1e-12: + s = 1.0 + r = np.abs(residuals) / s + w = np.where(r <= c, 1.0, c / r) + return np.asarray(w, dtype=np.float64) + + +def fit_sindy_weighted( + Theta: np.ndarray, + y: np.ndarray, + thresholds: Optional[List[float]] = None, + alpha: float = 1e-4, + max_iter: int = 25, + sample_weights: Optional[np.ndarray] = None, + n_robust_passes: int = 2, +) -> List[dict]: + """Run SINDy with threshold grid and optional robust weighting. + + Two-stage robust fitting: + 1. First pass: OLS, compute residuals, compute Huber weights + 2. Second pass: weighted STLSQ with Huber weights + + Args: + Theta: (T, N) feature matrix. + y: (T,) target. + thresholds: list of threshold values. + alpha: ridge regularization. + max_iter: max STLSQ iterations. + sample_weights: optional (T,) pre-defined sample weights. + n_robust_passes: number of robust re-weighting passes (1 = skip). + + Returns: + results: list of dict per threshold with keys: + threshold, nz, r2, mae, coef, weights_used + """ + import pysindy as ps + + if thresholds is None: + thresholds = DEFAULT_THRESHOLDS + + # Normalise features for thresholding stability + std = np.std(Theta, axis=0) + std = np.where(std < 1e-8, 1.0, std) + Theta_s = Theta / std + + # Initialize weights + if sample_weights is not None: + w = np.asarray(sample_weights, dtype=np.float64).flatten() + w = w / np.mean(w) # normalize to mean=1 + else: + w = np.ones(Theta.shape[0], dtype=np.float64) + + # Robust re-weighting passes + for _ in range(n_robust_passes - 1): + # OLS on weighted data + Theta_w = Theta_s * np.sqrt(w)[:, None] + y_w = y * np.sqrt(w) + coef_ols, _, _, _ = np.linalg.lstsq(Theta_w, y_w, rcond=None) + coef_ols = coef_ols.flatten() / std + + resid = y - Theta @ coef_ols + w_new = _huber_weights(resid) + if sample_weights is not None: + w = w * w_new + else: + w = w_new + w = w / np.mean(w) + + results = [] + for th in thresholds: + if np.max(w) > 1e-8: + # Weighted STLSQ: apply weights via sample_weight + opt = ps.STLSQ(threshold=th, alpha=alpha, max_iter=max_iter) + opt.fit(Theta_s, y, sample_weight=w) + coef = np.asarray(opt.coef_, dtype=np.float64).flatten() / std + else: + coef = np.zeros(Theta.shape[1], dtype=np.float64) + + y_pred = Theta @ coef + # Weighted R2 + ssr = float(np.sum(w * (y - y_pred) ** 2)) + sst = float(np.sum(w * (y - np.average(y, weights=w)) ** 2) + 1e-12) + r2 = 1.0 - ssr / sst + mae = float(np.mean(np.abs(y - y_pred))) + nz = int(np.sum(np.abs(coef) > 1e-8)) + + results.append({ + "threshold": float(th), + "nz": nz, + "r2": r2, + "mae": mae, + "coef": [float(c) for c in coef], + "weights_min": float(np.min(w)), + "weights_max": float(np.max(w)), + }) + + return results + + +# --------------------------------------------------------------------------- +# V2 feature matrix builder (configurable feature sets, time normalization) +# --------------------------------------------------------------------------- + +def get_feature_matrix_v2( + sensors: np.ndarray, # (T, 6) + forces: np.ndarray, # (T, 6) + actions_phys: np.ndarray, # (T, 3) physical omega + mu: float, + u0: float = U0, + alpha_mode: bool = False, + include_mu: bool = False, # default False for single-scene + include_cos_sin: bool = False, # default False (avoid masking linear structure) + use_time_norm: bool = False, # if True, use time-normalized deltas (da/dt_c) + feat_keys: Optional[List[str]] = None, # custom feat keys (default CORE_FEAT_KEYS_V2) + dt_c: float = 1.0, # control interval in T0 units + n_warmup: int = 2, + target_forces: Optional[np.ndarray] = None, # (T, 2) raw lattice target forces +) -> Tuple[np.ndarray, np.ndarray, np.ndarray, List[str], List[str]]: + """Build feature matrices with configurable feature sets. + + Uses CORE_FEAT_KEYS_V2 by default (no sin_ua/cos_ua, no mu). + For Illusion scenes with target forces, provide target_forces and + feat_keys=ILLUSION_FEAT_KEYS_V2 (or pass None to auto-detect). + For cross-Re joint fitting, set include_mu=True. + For time-normalized deltas, set use_time_norm=True and provide dt_c. + + Returns + ------- + Theta_front : (T-warmup, N_front) NO bias column + Theta_rear : (T-warmup, N_rear) WITH bias column + Y : (T-warmup, 3) target actions (physical omega) + feat_names_front, feat_names_rear + """ + from .feature_builder import ( + CORE_FEAT_KEYS_V2, TIME_FEAT_KEYS, MU_FEAT_KEYS, ALL_FEAT_KEYS_V2, + ILLUSION_FEAT_KEYS_V2, ILLUSION_ALL_FEAT_KEYS_V2, + ) + + if feat_keys is None: + if target_forces is not None: + # Illusion scenes: include target force features + feat_keys = ILLUSION_FEAT_KEYS_V2 + elif use_time_norm: + # Replace discrete da with time-normalized da/dt + feat_keys = [k for k in CORE_FEAT_KEYS_V2 if not k.startswith("da")] + feat_keys += TIME_FEAT_KEYS + else: + feat_keys = CORE_FEAT_KEYS_V2 + + if include_mu: + # Add mu features if not already in feat_keys + for mk in MU_FEAT_KEYS: + if mk not in feat_keys: + feat_keys = feat_keys + [mk] + + T = sensors.shape[0] + a_prev = np.zeros((T, 3), dtype=np.float64) + a_prev2 = np.zeros((T, 3), dtype=np.float64) + a_prev[1:] = actions_phys[:-1] + a_prev2[2:] = actions_phys[:-2] + + dim = compute_dimensionless(sensors, forces, u0=u0, d=20.0) + sym = compute_features( + dim, a_prev, a_prev2, mu, + alpha_mode=alpha_mode, + include_mu=include_mu, + include_cos_sin=include_cos_sin, + dt_c=dt_c, + u0=u0, + target_forces=target_forces, + ) + + Theta_f = build_feature_matrix(sym, feat_keys, add_bias=False) + Theta_r = build_feature_matrix(sym, feat_keys, add_bias=True) + + feat_names_front = get_feature_names(feat_keys, add_bias=False) + feat_names_rear = get_feature_names(feat_keys, add_bias=True) + + return (Theta_f[n_warmup:], Theta_r[n_warmup:], + actions_phys[n_warmup:], + feat_names_front, feat_names_rear) + + +# --------------------------------------------------------------------------- +# Derivative-mode feature builders: fit d(alpha)/dt = g(physics_state) +# No action history in input features; Y is time-normalized action derivative. +# --------------------------------------------------------------------------- + +def compute_action_deriv( + actions_phys: np.ndarray, # (T, 3) physical omega + dt_c: float, # control interval in T0 units + u0: float = U0, + center_diff: bool = False, # if True, use (alpha(t) - alpha(t-2))/(2*dt_c) +) -> np.ndarray: + """Compute time-normalized action derivative d(alpha)/dt. + + forward_diff: d(alpha)/dt ~ (alpha(t) - alpha(t-1)) / dt_c + center_diff: d(alpha)/dt ~ (alpha(t) - alpha(t-2)) / (2*dt_c) + + Returns (T, 3) array, with first row(s) zero-padded. + """ + alpha = np.asarray(actions_phys, dtype=np.float64) / u0 # non-dim + T = alpha.shape[0] + deriv = np.zeros_like(alpha) + if center_diff and T >= 3: + deriv[2:] = (alpha[2:] - alpha[:-2]) / (2.0 * dt_c) + elif T >= 2: + deriv[1:] = (alpha[1:] - alpha[:-1]) / dt_c + return deriv + + +def get_feature_matrix_deriv( + sensors: np.ndarray, # (T, 6) + forces: np.ndarray, # (T, 6) + actions_phys: np.ndarray, # (T, 3) physical omega + mu: float, + u0: float = U0, + dt_c: float = 1.0, # control interval in T0 units + feat_keys: Optional[List[str]] = None, # default PHYSICS_FEAT_KEYS + include_mu: bool = False, # add mu modulation features (for cross-Re joint) + target_forces: Optional[np.ndarray] = None, # (T, 2) for Illusion + n_warmup: int = 2, + center_diff: bool = False, + augment_level: int = 0, # 0=static, 1=+lags, 2=+derivs, 3=both, 4=+action_lag + output_mode: str = "deriv", # "deriv": predict d(alpha)/dt; "absolute": predict alpha directly +) -> Tuple[np.ndarray, np.ndarray, np.ndarray, List[str], List[str]]: + """Build feature matrices for phase-state SINDy. + + Input features: physics state with optional temporal context. + + Parameters + ---------- + output_mode : str + "deriv": Y = d(alpha)/dt (time-normalized). Closed-loop needs integration. + "absolute": Y = alpha (non-dimensional action). No integration needed. + + augment_level : int + 0: PHYSICS_FEAT_KEYS only (static, no memory) + 1: + lag-1 obs features (adds temporal context) + 2: + obs derivative features (adds rate information) + 3: + lag-1 + derivative (full temporal context) + 4: + action lag (aF_lag1 etc., for ablation comparison against old approach) + """ + from .feature_builder import ( + PHYSICS_FEAT_KEYS, ILLUSION_ERR_KEYS, MU_FEAT_KEYS, + AUG_LEVEL_1_KEYS, AUG_LEVEL_2_KEYS, + AUG_LEVEL_3_KEYS, AUG_LEVEL_4_KEYS, + ILLUSION_AUG_LEVEL_1_KEYS, ILLUSION_AUG_LEVEL_2_KEYS, + ILLUSION_AUG_LEVEL_3_KEYS, ILLUSION_AUG_LEVEL_4_KEYS, + ) + + # Select feature key set based on augment_level + _AUG_MAP = {0: PHYSICS_FEAT_KEYS, 1: AUG_LEVEL_1_KEYS, + 2: AUG_LEVEL_2_KEYS, 3: AUG_LEVEL_3_KEYS, 4: AUG_LEVEL_4_KEYS} + _AUG_ILLUSION_MAP = {0: ILLUSION_ERR_KEYS, 1: ILLUSION_AUG_LEVEL_1_KEYS, + 2: ILLUSION_AUG_LEVEL_2_KEYS, 3: ILLUSION_AUG_LEVEL_3_KEYS, + 4: ILLUSION_AUG_LEVEL_4_KEYS} + + if feat_keys is None: + if target_forces is not None: + feat_keys = _AUG_ILLUSION_MAP.get(augment_level, ILLUSION_AUG_LEVEL_3_KEYS) + else: + feat_keys = _AUG_MAP.get(augment_level, AUG_LEVEL_3_KEYS) + + if include_mu: + for mk in MU_FEAT_KEYS: + if mk not in feat_keys: + feat_keys = feat_keys + [mk] + + if include_mu: + for mk in MU_FEAT_KEYS: + if mk not in feat_keys: + feat_keys = feat_keys + [mk] + + T = sensors.shape[0] + # a_prev/a_prev2 only used for computing the DERIVATIVE target Y, not in Theta + a_prev = np.zeros((T, 3), dtype=np.float64) + a_prev2 = np.zeros((T, 3), dtype=np.float64) + a_prev[1:] = actions_phys[:-1] + a_prev2[2:] = actions_phys[:-2] + + dim = compute_dimensionless(sensors, forces, u0=u0, d=20.0) + sym = compute_features( + dim, a_prev, a_prev2, mu, + alpha_mode=False, include_mu=include_mu, + include_cos_sin=False, dt_c=dt_c, u0=u0, + target_forces=target_forces, + sensors_raw=sensors, forces_raw=forces, + ) + + Theta_f = build_feature_matrix(sym, feat_keys, add_bias=False) + Theta_r = build_feature_matrix(sym, feat_keys, add_bias=True) + + feat_names_front = get_feature_names(feat_keys, add_bias=False) + feat_names_rear = get_feature_names(feat_keys, add_bias=True) + + # Y: depends on output_mode + if output_mode == "absolute": + Y = np.asarray(actions_phys, dtype=np.float64) / u0 # non-dim alpha (absolute action) + else: + Y = compute_action_deriv(actions_phys, dt_c, u0=u0, center_diff=center_diff) + + return (Theta_f[n_warmup:], Theta_r[n_warmup:], + Y[n_warmup:], + feat_names_front, feat_names_rear) diff --git a/src/SR_analysis/core/g_operator.py b/src/SR_analysis/core/g_operator.py new file mode 100644 index 0000000..0a2f956 --- /dev/null +++ b/src/SR_analysis/core/g_operator.py @@ -0,0 +1,191 @@ +"""G-operator and equivariance tools. + +Provides G-operator transformations, dimensionless conversion, +and equivariance diagnostics for PPO control laws. +""" +from __future__ import annotations + +from typing import Any, Dict, Optional, Tuple + +import numpy as np + +from .feature_builder import compute_dimensionless as _compute_dimless + + +def apply_G_alpha(alpha: np.ndarray) -> np.ndarray: + """Apply mirror G to action: [aF, aT, aB] -> [-aF, -aB, -aT].""" + return np.array([-alpha[0], -alpha[2], -alpha[1]], dtype=alpha.dtype) + + +def apply_G_raw(obs_slice: np.ndarray, + a_prev: np.ndarray, + a_prev2: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """Apply G to raw obs slice [sensor(6)+force(6)] and action arrays. + + Parameters + ---------- + obs_slice : (12,) raw obs [s0_ux,uy, s1_ux,uy, s2_ux,uy, f0_fx,fy, f1_fx,fy, f2_fx,fy] + a_prev : (3,) physical omega at t-1 + a_prev2 : (3,) physical omega at t-2 + + Returns + ------- + G_obs : (12,) transformed obs slice + G_a_prev : (3,) transformed a_prev + G_a_prev2 : (3,) transformed a_prev2 + """ + G_obs = np.zeros(12, dtype=np.float64) + # sensors: swap top(0,1) <-> bottom(4,5), negate v + G_obs[0] = obs_slice[4] + G_obs[1] = -obs_slice[5] + G_obs[2] = obs_slice[2] + G_obs[3] = -obs_slice[3] + G_obs[4] = obs_slice[0] + G_obs[5] = -obs_slice[1] + # forces: swap bottom(2,3) <-> top(4,5), negate fy + G_obs[6] = obs_slice[6] + G_obs[7] = -obs_slice[7] + G_obs[8] = obs_slice[10] + G_obs[9] = -obs_slice[11] + G_obs[10] = obs_slice[8] + G_obs[11] = -obs_slice[9] + + G_a_prev = np.array([-a_prev[0], -a_prev[2], -a_prev[1]], dtype=np.float64) + G_a_prev2 = np.array([-a_prev2[0], -a_prev2[2], -a_prev2[1]], dtype=np.float64) + return G_obs, G_a_prev, G_a_prev2 + + +def check_equivariance( + model: Any, + obs_slice_series: np.ndarray, # (T, 12) raw obs + actions_phys: np.ndarray, # (T, 3) physical omega + norm: dict, + action_scale: float = 8.0, + action_bias: Tuple[float, float, float] = (0.0, -4.0, 4.0), + u0: float = 0.01, +) -> Dict[str, float]: + """Check G-equivariance of a PPO model over a time series. + + Returns dict with front/rear equivariance errors. + """ + from .cfd_interface import build_observation, action_to_physical + + T = min(obs_slice_series.shape[0], actions_phys.shape[0]) + ef, eb, et = [], [], [] + + for t in range(2, T): + # Get current obs + osl = obs_slice_series[t] + a_prev = actions_phys[t - 1] if t > 0 else actions_phys[0] + a_prev2 = actions_phys[t - 2] if t > 1 else actions_phys[0] + + # Predict action for current state + obs = build_observation(osl, norm) + act, _ = model.predict(obs, deterministic=True) + act = act.astype(np.float32).flatten() + alpha = action_to_physical(act.reshape(1, 3), + scale=action_scale, bias=action_bias, u0=u0).flatten() + + # Apply G to state + G_obs, _, _ = apply_G_raw(osl, a_prev, a_prev2) + obs_G = build_observation(G_obs, norm) + act_G, _ = model.predict(obs_G, deterministic=True) + act_G = act_G.astype(np.float32).flatten() + alpha_G = action_to_physical(act_G.reshape(1, 3), + scale=action_scale, bias=action_bias, u0=u0).flatten() + + # Expected: G(alpha) = [-aF, -aB, -aT] + expected = apply_G_alpha(alpha) + + ef.append(abs(float(alpha_G[0]) - float(expected[0]))) + eb.append(abs(float(alpha_G[1]) - float(expected[1]))) + et.append(abs(float(alpha_G[2]) - float(expected[2]))) + + ef_arr = np.array(ef) + eb_arr = np.array(eb) + et_arr = np.array(et) + alpha_range = float(np.max(np.abs(actions_phys[2:]))) + + return { + "front_mean_abs_error": float(np.mean(ef_arr)), + "front_rel_error": float(np.mean(ef_arr) / (alpha_range + 1e-12)), + "rear_bottom_rel_error": float(np.mean(eb_arr) / (alpha_range + 1e-12)), + "rear_top_rel_error": float(np.mean(et_arr) / (alpha_range + 1e-12)), + "alpha_range": alpha_range, + } + + +def diagnose_one_re(model, ff, target_states, norm, config, n_steps=150) -> dict: + """Run PPO inference and check equivariance. + + Parameters + ---------- + model : loaded PPO model + ff : FlowField instance (must be at saved checkpoint state) + target_states : (FIFO_LEN, 6) target sensor signals + norm : norm dict + config : scene config dict with action_scale, action_bias, u0, etc. + + Returns + ------- + dict with equivariance metrics. + """ + from collections import deque + from .cfd_interface import (build_observation, scale_action, + action_to_physical, compute_similarity) + + action_scale = config.get("action_scale", 8.0) + action_bias = config.get("action_bias", (0.0, -4.0, 4.0)) + u0 = config.get("u0", 0.01) + sample_interval = config.get("sample_interval", 800) + fifo_len = config.get("fifo_len", 150) + n_obj_total = config.get("n_objects_total", 7) + + ff.restore_ddf() + ff.apply_ddf() + + # Bias FIFO init + fifo = deque(maxlen=fifo_len) + bias_arr = scale_action(np.zeros(3, dtype=np.float32), + scale=action_scale, bias=action_bias, + u0=u0, n_total_bodies=n_obj_total) + for _ in range(fifo_len): + ff.run(sample_interval, bias_arr) + fifo.append(ff.obs.copy()[2:14]) + + # Inference + obs_array = [] + action_array = [] + obs = np.zeros(12, dtype=np.float32) + + for _ in range(n_steps): + act, _ = model.predict(obs, deterministic=True) + act = act.astype(np.float32).flatten() + action_array.append(act.copy()) + + action_arr = scale_action(act, scale=action_scale, bias=action_bias, + u0=u0, n_total_bodies=n_obj_total) + ff.context.push() + ff.run(sample_interval, action_arr) + ff.context.pop() + + obs_slice = ff.obs.copy()[2:14] + fifo.append(obs_slice) + obs_array.append(obs_slice) + obs = build_observation(obs_slice, norm) + + obs_series = np.array(obs_array, dtype=np.float64) + actions_phys = action_to_physical(np.array(action_array), + scale=action_scale, bias=action_bias, u0=u0) + states_arr = np.array(list(fifo), dtype=np.float32) + sim = compute_similarity(target_states, states_arr[:, 0:6], + config.get("conv_len", 30)) + + # Equivariance check + eq = check_equivariance(model, obs_series, actions_phys, norm, + action_scale, action_bias, u0) + + return { + "similarity": sim, + "equivariance": eq, + } diff --git a/src/SR_analysis/data/degradation_analysis/action_timeseries.png b/src/SR_analysis/data/degradation_analysis/action_timeseries.png new file mode 100644 index 0000000..ec84e66 Binary files /dev/null and b/src/SR_analysis/data/degradation_analysis/action_timeseries.png differ diff --git a/src/SR_analysis/data/degradation_analysis/autocorrelation.png b/src/SR_analysis/data/degradation_analysis/autocorrelation.png new file mode 100644 index 0000000..ce4447b Binary files /dev/null and b/src/SR_analysis/data/degradation_analysis/autocorrelation.png differ diff --git a/src/SR_analysis/data/degradation_analysis/fft_spectra.png b/src/SR_analysis/data/degradation_analysis/fft_spectra.png new file mode 100644 index 0000000..9ecb6e4 Binary files /dev/null and b/src/SR_analysis/data/degradation_analysis/fft_spectra.png differ diff --git a/src/SR_analysis/data/diagnostics/phase_portrait_illusion_0.75L.png b/src/SR_analysis/data/diagnostics/phase_portrait_illusion_0.75L.png new file mode 100644 index 0000000..7d7ec16 Binary files /dev/null and b/src/SR_analysis/data/diagnostics/phase_portrait_illusion_0.75L.png differ diff --git a/src/SR_analysis/data/diagnostics/phase_portrait_illusion_1.5L.png b/src/SR_analysis/data/diagnostics/phase_portrait_illusion_1.5L.png new file mode 100644 index 0000000..02483e1 Binary files /dev/null and b/src/SR_analysis/data/diagnostics/phase_portrait_illusion_1.5L.png differ diff --git a/src/SR_analysis/data/diagnostics/phase_portrait_illusion_1L.png b/src/SR_analysis/data/diagnostics/phase_portrait_illusion_1L.png new file mode 100644 index 0000000..f8a0e2b Binary files /dev/null and b/src/SR_analysis/data/diagnostics/phase_portrait_illusion_1L.png differ diff --git a/src/SR_analysis/data/diagnostics/timeseries_full_illusion_0.75L.png b/src/SR_analysis/data/diagnostics/timeseries_full_illusion_0.75L.png new file mode 100644 index 0000000..c3a5df2 Binary files /dev/null and b/src/SR_analysis/data/diagnostics/timeseries_full_illusion_0.75L.png differ diff --git a/src/SR_analysis/data/diagnostics/timeseries_full_illusion_1.5L.png b/src/SR_analysis/data/diagnostics/timeseries_full_illusion_1.5L.png new file mode 100644 index 0000000..ef229b8 Binary files /dev/null and b/src/SR_analysis/data/diagnostics/timeseries_full_illusion_1.5L.png differ diff --git a/src/SR_analysis/data/diagnostics/timeseries_full_illusion_1L.png b/src/SR_analysis/data/diagnostics/timeseries_full_illusion_1L.png new file mode 100644 index 0000000..6f4d6a5 Binary files /dev/null and b/src/SR_analysis/data/diagnostics/timeseries_full_illusion_1L.png differ diff --git a/src/SR_analysis/data/illusion/illusion_0.5L/target_harmonics.json b/src/SR_analysis/data/illusion/illusion_0.5L/target_harmonics.json new file mode 100644 index 0000000..c7ee02c --- /dev/null +++ b/src/SR_analysis/data/illusion/illusion_0.5L/target_harmonics.json @@ -0,0 +1,50 @@ +[ + { + "dc": 0.0029594360229869684, + "amps": [ + 4.482508629307246e-06, + 1.7559728832214555e-06, + 1.695399969073208e-06, + 4.863359419766791e-07, + 4.0744896335315875e-07 + ], + "freqs": [ + 0.08666666666666667, + 0.18000000000000002, + 0.09333333333333334, + 0.1, + 0.08 + ], + "phases": [ + -2.223160683015736, + -3.090016544658663, + -1.4396296733151448, + -1.3066393251376214, + 1.6772756869554482 + ] + }, + { + "dc": 1.3870471496678268e-05, + "amps": [ + 0.00028510443996025985, + 0.0002599505102795033, + 9.365825828740738e-05, + 8.877059656510341e-05, + 5.6681594533049977e-05 + ], + "freqs": [ + 0.04666666666666667, + 0.04, + 0.05333333333333334, + 0.03333333333333333, + 0.060000000000000005 + ], + "phases": [ + -2.111421674988832, + 0.931445743532081, + -2.026139500541211, + 0.8174729094507539, + -1.9494987485100541 + ] + } +] \ No newline at end of file diff --git a/src/SR_analysis/data/illusion/illusion_0.6L/target_harmonics.json b/src/SR_analysis/data/illusion/illusion_0.6L/target_harmonics.json new file mode 100644 index 0000000..d81b4c4 --- /dev/null +++ b/src/SR_analysis/data/illusion/illusion_0.6L/target_harmonics.json @@ -0,0 +1,50 @@ +[ + { + "dc": 0.0034837019443511963, + "amps": [ + 1.1376483917607967e-05, + 7.248102177867125e-06, + 5.838593601492066e-06, + 3.099357973227853e-06, + 2.721364398772006e-06 + ], + "freqs": [ + 0.08, + 0.07333333333333333, + 0.09333333333333334, + 0.18000000000000002, + 0.1 + ], + "phases": [ + -1.0408841490876113, + 1.9485158983384934, + -1.3904977741558484, + 2.7148922736497454, + -1.2577909435667654 + ] + }, + { + "dc": 2.0249660774425138e-05, + "amps": [ + 0.0006472307684973089, + 0.0001580755533972986, + 9.359867367951902e-05, + 7.772244469336163e-05, + 5.558302481685001e-05 + ], + "freqs": [ + 0.04, + 0.03333333333333333, + 0.04666666666666667, + 0.02666666666666667, + 0.02 + ], + "phases": [ + 3.026701709868741, + -0.09658000009988132, + 3.0095201121212978, + -0.07819925121021988, + -0.05948277368734109 + ] + } +] \ No newline at end of file diff --git a/src/SR_analysis/data/illusion/illusion_0.75L/target_harmonics.json b/src/SR_analysis/data/illusion/illusion_0.75L/target_harmonics.json new file mode 100644 index 0000000..08c946f --- /dev/null +++ b/src/SR_analysis/data/illusion/illusion_0.75L/target_harmonics.json @@ -0,0 +1,50 @@ +[ + { + "dc": 0.004293269868940115, + "amps": [ + 3.8063591256693715e-05, + 8.663235430632004e-06, + 7.901108470774483e-06, + 7.1510384113964075e-06, + 6.272449978203118e-06 + ], + "freqs": [ + 0.06666666666666667, + 0.09333333333333334, + 0.08666666666666667, + 0.18000000000000002, + 0.006666666666666667 + ], + "phases": [ + 2.9599042697964997, + -1.7903030042984585, + 1.1028484616237284, + 1.731865132582606, + -2.5926944354040096 + ] + }, + { + "dc": -1.1502189348296572e-06, + "amps": [ + 0.0011732713800496743, + 8.715048457232978e-06, + 8.113829684732725e-06, + 2.805951507723943e-06, + 2.2956073119401994e-06 + ], + "freqs": [ + 0.03333333333333333, + 0.02666666666666667, + 0.04, + 0.02, + 0.1 + ], + "phases": [ + -1.225940564415875, + 2.5874876850956996, + -1.9715232735875732, + -1.0313721340317885, + -2.2437335289130216 + ] + } +] \ No newline at end of file diff --git a/src/SR_analysis/data/illusion/illusion_0.8L/target_harmonics.json b/src/SR_analysis/data/illusion/illusion_0.8L/target_harmonics.json new file mode 100644 index 0000000..0bbbccf --- /dev/null +++ b/src/SR_analysis/data/illusion/illusion_0.8L/target_harmonics.json @@ -0,0 +1,50 @@ +[ + { + "dc": 0.004459296974043052, + "amps": [ + 4.365837863945567e-05, + 1.6627461404015874e-05, + 1.499327969764015e-05, + 1.0882240141500203e-05, + 8.648375991472251e-06 + ], + "freqs": [ + 0.09333333333333334, + 0.1, + 0.13333333333333333, + 0.08666666666666667, + 0.10666666666666667 + ], + "phases": [ + -0.955645707880348, + 2.2853500968785534, + 0.39850354801733023, + -0.6758960563529793, + 2.500473431154046 + ] + }, + { + "dc": -9.51454075887644e-05, + "amps": [ + 0.0012646521264823782, + 0.00021337454745985233, + 0.00018204900807997863, + 0.0001045493570771146, + 9.486869355170616e-05 + ], + "freqs": [ + 0.04666666666666667, + 0.05333333333333334, + 0.04, + 0.03333333333333333, + 0.060000000000000005 + ], + "phases": [ + 3.0652144428661194, + -0.07281059956203381, + 3.0763553688568286, + 3.0963827460231994, + -0.08166873030294015 + ] + } +] \ No newline at end of file diff --git a/src/SR_analysis/data/illusion/illusion_1.2L/target_harmonics.json b/src/SR_analysis/data/illusion/illusion_1.2L/target_harmonics.json new file mode 100644 index 0000000..f5b1679 --- /dev/null +++ b/src/SR_analysis/data/illusion/illusion_1.2L/target_harmonics.json @@ -0,0 +1,50 @@ +[ + { + "dc": 0.006754336698601643, + "amps": [ + 0.0001205644768172709, + 0.00010325204771730541, + 6.1032211125949833e-05, + 4.133092864305172e-05, + 3.322629507619568e-05 + ], + "freqs": [ + 0.06666666666666667, + 0.07333333333333333, + 0.13333333333333333, + 0.060000000000000005, + 0.08 + ], + "phases": [ + 0.21557592230485592, + -2.886865293479548, + -1.017309664173338, + 0.18116041037477737, + -2.833486698409445 + ] + }, + { + "dc": -0.00011794015166136282, + "amps": [ + 0.0027813392111168526, + 0.0008110970246664206, + 0.0005849442269674323, + 0.00035855985851303973, + 0.00033022754730012617 + ], + "freqs": [ + 0.03333333333333333, + 0.04, + 0.02666666666666667, + 0.02, + 0.04666666666666667 + ], + "phases": [ + -2.728322858690305, + 0.49031498750676245, + -2.808700524748656, + -2.886553982431292, + 0.565211864386275 + ] + } +] \ No newline at end of file diff --git a/src/SR_analysis/data/illusion/illusion_1.5L/target_harmonics.json b/src/SR_analysis/data/illusion/illusion_1.5L/target_harmonics.json new file mode 100644 index 0000000..110df92 --- /dev/null +++ b/src/SR_analysis/data/illusion/illusion_1.5L/target_harmonics.json @@ -0,0 +1,50 @@ +[ + { + "dc": 0.008447802712519964, + "amps": [ + 0.0002532245295392234, + 0.00014147298112950578, + 0.0001297400791941424, + 6.434300108105502e-05, + 5.7950849123282916e-05 + ], + "freqs": [ + 0.08, + 0.07333333333333333, + 0.18000000000000002, + 0.08666666666666667, + 0.36000000000000004 + ], + "phases": [ + 0.5998824737031915, + -2.5827112781492545, + -3.0945367379195816, + 0.6391142016853608, + 0.10751503340383528 + ] + }, + { + "dc": 0.00011803933939518174, + "amps": [ + 0.004330276189305638, + 0.000976254738854663, + 0.0006186459734055383, + 0.0004722765324672463, + 0.000331904307497245 + ], + "freqs": [ + 0.04, + 0.03333333333333333, + 0.04666666666666667, + 0.02666666666666667, + 0.02 + ], + "phases": [ + -2.551505303958517, + 0.5007165195151991, + -2.466226000290465, + 0.4069960130268731, + 0.3090395499543929 + ] + } +] \ No newline at end of file diff --git a/src/SR_analysis/data/illusion/illusion_1L/target_harmonics.json b/src/SR_analysis/data/illusion/illusion_1L/target_harmonics.json new file mode 100644 index 0000000..a3c183d --- /dev/null +++ b/src/SR_analysis/data/illusion/illusion_1L/target_harmonics.json @@ -0,0 +1,50 @@ +[ + { + "dc": 0.005645164093002677, + "amps": [ + 9.94330093591307e-05, + 3.232916972111346e-05, + 1.3215355231141013e-05, + 1.1981542249045906e-05, + 1.1909839518599968e-05 + ], + "freqs": [ + 0.08, + 0.13333333333333333, + 0.26666666666666666, + 0.2733333333333334, + 0.08666666666666667 + ], + "phases": [ + 1.7159609635464452, + -0.2507805590458965, + 1.0324605783803387, + -1.6430097798845071, + -1.2507241172270498 + ] + }, + { + "dc": -7.872594342188676e-06, + "amps": [ + 0.0020951717499817367, + 0.00011698728994078175, + 9.559502592412231e-05, + 5.94451334704438e-05, + 4.6248624934531934e-05 + ], + "freqs": [ + 0.04, + 0.04666666666666667, + 0.03333333333333333, + 0.05333333333333334, + 0.02666666666666667 + ], + "phases": [ + -1.9167863100531028, + 1.3060842750219288, + -2.0155661953529633, + 1.3740336240558457, + -2.1415896469819797 + ] + } +] \ No newline at end of file diff --git a/src/SR_analysis/data/illusion/illusion_2L/target_harmonics.json b/src/SR_analysis/data/illusion/illusion_2L/target_harmonics.json new file mode 100644 index 0000000..a8f0da0 --- /dev/null +++ b/src/SR_analysis/data/illusion/illusion_2L/target_harmonics.json @@ -0,0 +1,50 @@ +[ + { + "dc": 0.011301154401153327, + "amps": [ + 0.0005916813028667388, + 0.0003228542347087601, + 0.00011299686050540704, + 0.00010933004657394343, + 9.57751063779222e-05 + ], + "freqs": [ + 0.060000000000000005, + 0.18000000000000002, + 0.35333333333333333, + 0.06666666666666667, + 0.36000000000000004 + ], + "phases": [ + -1.2029089114719096, + 0.10667750417539099, + 0.8128110625734376, + 1.9355161635025424, + -1.7526560628277699 + ] + }, + { + "dc": 0.000209219001474897, + "amps": [ + 0.00560199847829251, + 0.0038799719967085497, + 0.0017754993231967535, + 0.0012951582037757647, + 0.001094199761464694 + ], + "freqs": [ + 0.03333333333333333, + 0.02666666666666667, + 0.04, + 0.02, + 0.04666666666666667 + ], + "phases": [ + -1.8630748640530403, + 1.1749393426220804, + -1.7802277933394777, + 1.028375164210408, + -1.7139048570401034 + ] + } +] \ No newline at end of file diff --git a/src/SR_analysis/data/steady/steady/norm.json b/src/SR_analysis/data/steady/steady/norm.json new file mode 100644 index 0000000..247cdf8 --- /dev/null +++ b/src/SR_analysis/data/steady/steady/norm.json @@ -0,0 +1,24 @@ +{ + "force_norm_fact": 1.0, + "sens_deviation": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "sens_norm_fact": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ], + "action_bias": [ + 0.0, + -5.1, + 5.1 + ] +} \ No newline at end of file diff --git a/src/SR_analysis/data/steady/steady/result.json b/src/SR_analysis/data/steady/steady/result.json new file mode 100644 index 0000000..8e90f42 --- /dev/null +++ b/src/SR_analysis/data/steady/steady/result.json @@ -0,0 +1,11 @@ +{ + "scene": "steady", + "controlled": true, + "source": "open_loop", + "action_constant": [ + 0.0, + -0.051, + 0.051 + ], + "note": "open-loop constant rotation, no PPO model" +} \ No newline at end of file diff --git a/src/SR_analysis/old_data/pareto_karman_re100.json b/src/SR_analysis/old_data/pareto_karman_re100.json new file mode 100644 index 0000000..de29c70 --- /dev/null +++ b/src/SR_analysis/old_data/pareto_karman_re100.json @@ -0,0 +1,118 @@ +{ + "scene": "karman_re100", + "channels": [ + { + "channel": "front", + "best_r2": 0.9950013415086292, + "best_nz": 21, + "pareto": [ + { + "nz": 2, + "r2": 0.8681678005649112 + }, + { + "nz": 3, + "r2": 0.9692414821446799 + }, + { + "nz": 4, + "r2": 0.989378747581318 + }, + { + "nz": 6, + "r2": 0.9908017426802015 + }, + { + "nz": 12, + "r2": 0.9939890287801123 + }, + { + "nz": 13, + "r2": 0.9947660566975605 + }, + { + "nz": 18, + "r2": 0.9949963098276752 + }, + { + "nz": 21, + "r2": 0.9950013415086292 + } + ] + }, + { + "channel": "top", + "best_r2": 0.9928439394510484, + "best_nz": 22, + "pareto": [ + { + "nz": 1, + "r2": 0.44188145385324007 + }, + { + "nz": 2, + "r2": 0.9285296099294669 + }, + { + "nz": 6, + "r2": 0.9812946866555053 + }, + { + "nz": 12, + "r2": 0.9909377708950154 + }, + { + "nz": 17, + "r2": 0.9927843560231949 + }, + { + "nz": 20, + "r2": 0.992824536423302 + }, + { + "nz": 22, + "r2": 0.9928439394510484 + } + ] + }, + { + "channel": "bottom", + "best_r2": 0.9965335484991993, + "best_nz": 22, + "pareto": [ + { + "nz": 1, + "r2": 0.8456588970543057 + }, + { + "nz": 2, + "r2": 0.9030386794798415 + }, + { + "nz": 8, + "r2": 0.9947803148283133 + }, + { + "nz": 10, + "r2": 0.9962551509064745 + }, + { + "nz": 13, + "r2": 0.9963877299663628 + }, + { + "nz": 16, + "r2": 0.9964335809851655 + }, + { + "nz": 18, + "r2": 0.9965311727162228 + }, + { + "nz": 22, + "r2": 0.9965335484991993 + } + ] + } + ] +} \ No newline at end of file diff --git a/src/SR_analysis/old_data/pareto_vortex_lamb.json b/src/SR_analysis/old_data/pareto_vortex_lamb.json new file mode 100644 index 0000000..0970f36 --- /dev/null +++ b/src/SR_analysis/old_data/pareto_vortex_lamb.json @@ -0,0 +1,110 @@ +{ + "scene": "vortex_lamb", + "channels": [ + { + "channel": "front", + "best_r2": 0.9035051679446613, + "best_nz": 21, + "pareto": [ + { + "nz": 3, + "r2": 0.8536388609680176 + }, + { + "nz": 8, + "r2": 0.8957625139671737 + }, + { + "nz": 9, + "r2": 0.8985847902462778 + }, + { + "nz": 16, + "r2": 0.9017891237504362 + }, + { + "nz": 20, + "r2": 0.9035028044287374 + }, + { + "nz": 21, + "r2": 0.9035051679446613 + } + ] + }, + { + "channel": "top", + "best_r2": 0.979810012198325, + "best_nz": 22, + "pareto": [ + { + "nz": 0, + "r2": -0.2926478447353347 + }, + { + "nz": 1, + "r2": 0.9262664550660489 + }, + { + "nz": 2, + "r2": 0.9602153300731789 + }, + { + "nz": 5, + "r2": 0.9685659511773673 + }, + { + "nz": 6, + "r2": 0.9752589669369754 + }, + { + "nz": 19, + "r2": 0.9796029724451923 + }, + { + "nz": 20, + "r2": 0.9797408009698567 + }, + { + "nz": 22, + "r2": 0.979810012198325 + } + ] + }, + { + "channel": "bottom", + "best_r2": 0.9334422885170565, + "best_nz": 22, + "pareto": [ + { + "nz": 0, + "r2": -5.4349952892154265 + }, + { + "nz": 1, + "r2": 0.6935730348615337 + }, + { + "nz": 5, + "r2": 0.8721441125489685 + }, + { + "nz": 7, + "r2": 0.8963211646824344 + }, + { + "nz": 11, + "r2": 0.9294742132351927 + }, + { + "nz": 15, + "r2": 0.9318911728011019 + }, + { + "nz": 22, + "r2": 0.9334422885170565 + } + ] + } + ] +} \ No newline at end of file diff --git a/src/SR_analysis/old_data/pareto_vortex_taylor.json b/src/SR_analysis/old_data/pareto_vortex_taylor.json new file mode 100644 index 0000000..debc8df --- /dev/null +++ b/src/SR_analysis/old_data/pareto_vortex_taylor.json @@ -0,0 +1,58 @@ +{ + "scene": "vortex_taylor", + "channels": [ + { + "channel": "front", + "best_r2": 0.9603622630700551, + "best_nz": 21, + "pareto": [ + { + "nz": 0, + "r2": -1762.7026716770156 + }, + { + "nz": 21, + "r2": 0.9603622630700551 + } + ] + }, + { + "channel": "top", + "best_r2": 0.809824114603052, + "best_nz": 22, + "pareto": [ + { + "nz": 0, + "r2": -3813.3981416722418 + }, + { + "nz": 1, + "r2": 4.909409545561516e-09 + }, + { + "nz": 22, + "r2": 0.809824114603052 + } + ] + }, + { + "channel": "bottom", + "best_r2": 0.6431303693566448, + "best_nz": 22, + "pareto": [ + { + "nz": 0, + "r2": -11389.175969107222 + }, + { + "nz": 1, + "r2": 2.5346330034814457e-08 + }, + { + "nz": 22, + "r2": 0.6431303693566448 + } + ] + } + ] +} \ No newline at end of file diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0000_pct00.png b/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0000_pct00.png new file mode 100644 index 0000000..7304b61 Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0000_pct00.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0060_pct12.png b/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0060_pct12.png new file mode 100644 index 0000000..b9e7461 Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0060_pct12.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0120_pct25.png b/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0120_pct25.png new file mode 100644 index 0000000..9c20c71 Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0120_pct25.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0180_pct37.png b/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0180_pct37.png new file mode 100644 index 0000000..1a952b1 Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0180_pct37.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0240_pct50.png b/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0240_pct50.png new file mode 100644 index 0000000..d7fbdce Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0240_pct50.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0300_pct62.png b/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0300_pct62.png new file mode 100644 index 0000000..430656d Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0300_pct62.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0360_pct75.png b/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0360_pct75.png new file mode 100644 index 0000000..e1706ff Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0360_pct75.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0420_pct87.png b/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0420_pct87.png new file mode 100644 index 0000000..41a93ea Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0420_pct87.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0479_pct99.png b/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0479_pct99.png new file mode 100644 index 0000000..864a2c7 Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_0.75L_PPO_step0479_pct99.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0000_pct00.png b/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0000_pct00.png new file mode 100644 index 0000000..ea8af43 Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0000_pct00.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0037_pct12.png b/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0037_pct12.png new file mode 100644 index 0000000..ac52f6e Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0037_pct12.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0074_pct24.png b/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0074_pct24.png new file mode 100644 index 0000000..4ce3518 Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0074_pct24.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0111_pct37.png b/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0111_pct37.png new file mode 100644 index 0000000..f384195 Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0111_pct37.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0148_pct49.png b/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0148_pct49.png new file mode 100644 index 0000000..e0a9d24 Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0148_pct49.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0185_pct61.png b/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0185_pct61.png new file mode 100644 index 0000000..47a64ee Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0185_pct61.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0222_pct74.png b/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0222_pct74.png new file mode 100644 index 0000000..5de46ac Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0222_pct74.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0259_pct86.png b/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0259_pct86.png new file mode 100644 index 0000000..edb7c83 Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0259_pct86.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0296_pct98.png b/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0296_pct98.png new file mode 100644 index 0000000..7745082 Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0296_pct98.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0299_pct99.png b/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0299_pct99.png new file mode 100644 index 0000000..0d6d033 Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_1.5L_PPO_step0299_pct99.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0000_pct00.png b/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0000_pct00.png new file mode 100644 index 0000000..02b0509 Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0000_pct00.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0040_pct12.png b/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0040_pct12.png new file mode 100644 index 0000000..05971bb Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0040_pct12.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0080_pct25.png b/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0080_pct25.png new file mode 100644 index 0000000..a2d893c Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0080_pct25.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0120_pct37.png b/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0120_pct37.png new file mode 100644 index 0000000..69f2a9b Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0120_pct37.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0160_pct50.png b/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0160_pct50.png new file mode 100644 index 0000000..d4ca01c Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0160_pct50.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0200_pct62.png b/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0200_pct62.png new file mode 100644 index 0000000..5605565 Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0200_pct62.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0240_pct75.png b/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0240_pct75.png new file mode 100644 index 0000000..d8741ce Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0240_pct75.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0280_pct87.png b/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0280_pct87.png new file mode 100644 index 0000000..a865ada Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0280_pct87.png differ diff --git a/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0319_pct99.png b/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0319_pct99.png new file mode 100644 index 0000000..f27b357 Binary files /dev/null and b/src/SR_analysis/old_data/ppo_step_images/illusion_1L_PPO_step0319_pct99.png differ diff --git a/src/SR_analysis/old_data/pysr_all_results.json b/src/SR_analysis/old_data/pysr_all_results.json new file mode 100644 index 0000000..9cfe32f --- /dev/null +++ b/src/SR_analysis/old_data/pysr_all_results.json @@ -0,0 +1,210 @@ +{ + "karman_re100_front": { + "output_label": "karman_re100_front", + "n_samples": 198, + "n_features": 1, + "feature_names": [ + "aF_lag1" + ], + "niterations": 50, + "best_equation": "(x0 * 0.01106541) / 1.106541", + "best_loss": 1.4959569e-19, + "r2": 1.0, + "equations": [ + { + "complexity": 1, + "loss": 0.00018302062, + "score": 0.0, + "equation": "-0.003915151", + "sympy_format": "-0.00391515100000000" + }, + { + "complexity": 3, + "loss": 1.7232441999999998e-19, + "score": 17.299498177983697, + "equation": "x0 * 0.01", + "sympy_format": "x0*0.01" + }, + { + "complexity": 5, + "loss": 1.4959569e-19, + "score": 0.07072130403388514, + "equation": "(x0 * 0.01106541) / 1.106541", + "sympy_format": "x0*0.01106541/1.106541" + } + ] + }, + "karman_re100_top": { + "output_label": "karman_re100_top", + "n_samples": 198, + "n_features": 7, + "feature_names": [ + "bias", + "u_a", + "Cd_rear", + "Cl_diff", + "aF_lag1", + "aB_lag1", + "aT_lag1" + ], + "niterations": 50, + "best_equation": "(x6 * -0.00835853) / -0.835853", + "best_loss": 2.8386383999999997e-18, + "r2": 1.0, + "equations": [ + { + "complexity": 1, + "loss": 0.00031669735, + "score": 0.0, + "equation": "0.041159406", + "sympy_format": "0.0411594060000000" + }, + { + "complexity": 3, + "loss": 3.732284e-18, + "score": 16.035973661600742, + "equation": "x6 * 0.01", + "sympy_format": "x6*0.01" + }, + { + "complexity": 5, + "loss": 2.8386383999999997e-18, + "score": 0.13684793905473897, + "equation": "(x6 * -0.00835853) / -0.835853", + "sympy_format": "x6*(-0.00835853)/(-0.835853)" + } + ] + }, + "illusion_1L_front": { + "output_label": "illusion_1L_front", + "n_samples": 198, + "n_features": 14, + "feature_names": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT" + ], + "niterations": 100, + "best_equation": "((0.0039467034 * (x8 + x8)) - ((x8 / (-0.9984451 - (x4 + x8))) * square(2.5222103e-6))) / 0.7893407", + "best_loss": 8.728418e-19, + "r2": 1.0, + "equations": [ + { + "complexity": 1, + "loss": 0.004748166, + "score": 0.0, + "equation": "0.0012775909", + "sympy_format": "0.00127759090000000" + }, + { + "complexity": 3, + "loss": 2.6305633999999998e-18, + "score": 17.564668394678097, + "equation": "x8 * 0.01", + "sympy_format": "x8*0.01" + }, + { + "complexity": 5, + "loss": 1.3032371999999998e-18, + "score": 0.35117336038102565, + "equation": "(0.007208903 * x8) / 0.7208903", + "sympy_format": "0.007208903*x8/0.7208903" + }, + { + "complexity": 7, + "loss": 1.1532010999999999e-18, + "score": 0.06115484118442259, + "equation": "((x8 + x8) * 0.0039467034) / 0.7893407", + "sympy_format": "(x8 + x8)*0.0039467034/0.7893407" + }, + { + "complexity": 9, + "loss": 1.1488888999999999e-18, + "score": 0.001873169616955362, + "equation": "(((x8 + x8) - 1.3038937e-7) * 0.0039467034) / 0.7893407", + "sympy_format": "(x8 + x8 - 1*1.3038937e-7)*0.0039467034/0.7893407" + }, + { + "complexity": 12, + "loss": 1.1395758999999998e-18, + "score": 0.0027130419227974806, + "equation": "(((x8 + x8) * 0.0039467034) - square(x7 * -3.0155393e-6)) / 0.7893407", + "sympy_format": "(-9.09347726984449e-12*x7**2 + (x8 + x8)*0.0039467034)/0.7893407" + }, + { + "complexity": 14, + "loss": 1.0790729e-18, + "score": 0.02727696454758814, + "equation": "((x8 + (x8 - square((x13 + x13) * -1.5024679e-5))) * 0.0039467034) / 0.7893407", + "sympy_format": "(x8 + x8 - 2.25740979053041e-10*(x13 + x13)**2)*0.0039467034/0.7893407" + }, + { + "complexity": 18, + "loss": 8.728418e-19, + "score": 0.05302580007984551, + "equation": "((0.0039467034 * (x8 + x8)) - ((x8 / (-0.9984451 - (x4 + x8))) * square(2.5222103e-6))) / 0.7893407", + "sympy_format": "(-6.36154479742609e-12*x8/(-(x4 + x8) - 0.9984451) + 0.0039467034*(x8 + x8))/0.7893407" + } + ] + }, + "illusion_1L_top": { + "output_label": "illusion_1L_top", + "n_samples": 198, + "n_features": 15, + "feature_names": [ + "bias", + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT" + ], + "niterations": 100, + "best_equation": "(x11 * 0.0121531105) / 1.215311", + "best_loss": 2.9856626999999997e-18, + "r2": 1.0, + "equations": [ + { + "complexity": 1, + "loss": 0.0034831127, + "score": 0.0, + "equation": "0.01056136", + "sympy_format": "0.0105613600000000" + }, + { + "complexity": 3, + "loss": 3.6416598e-18, + "score": 17.247131588103095, + "equation": "x11 * 0.01", + "sympy_format": "x11*0.01" + }, + { + "complexity": 5, + "loss": 2.9856626999999997e-18, + "score": 0.09930891723700508, + "equation": "(x11 * 0.0121531105) / 1.215311", + "sympy_format": "x11*0.0121531105/1.215311" + } + ] + } +} \ No newline at end of file diff --git a/src/SR_analysis/old_data/sindy_karman_v1.json b/src/SR_analysis/old_data/sindy_karman_v1.json new file mode 100644 index 0000000..b83c35d --- /dev/null +++ b/src/SR_analysis/old_data/sindy_karman_v1.json @@ -0,0 +1,2031 @@ +{ + "thresholds": [ + 0.0, + 0.001, + 0.002, + 0.005, + 0.01, + 0.015, + 0.02, + 0.03, + 0.05, + 0.1 + ], + "all_feature_names_front": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "sin_ua", + "cos_ua", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff" + ], + "all_feature_names_rear": [ + "bias", + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "sin_ua", + "cos_ua", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff" + ], + "per_scene": { + "karman_re50": { + "scene": "karman_re50", + "re_code": 50, + "mu": 0.04, + "n_samples": 198, + "n_features_front": 21, + "n_features_rear": 22, + "feature_names_front": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "sin_ua", + "cos_ua", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff" + ], + "feature_names_rear": [ + "bias", + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "sin_ua", + "cos_ua", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff" + ], + "front": { + "results": [ + { + "threshold": 0.0, + "nz": 21, + "r2": 0.998319769966323, + "mae": 0.007229121645243238 + }, + { + "threshold": 0.001, + "nz": 19, + "r2": 0.9983129034537462, + "mae": 0.00723533900123122 + }, + { + "threshold": 0.002, + "nz": 18, + "r2": 0.9982861124400884, + "mae": 0.00729235470189541 + }, + { + "threshold": 0.005, + "nz": 4, + "r2": 0.9974439573320747, + "mae": 0.008776250231333573 + }, + { + "threshold": 0.01, + "nz": 3, + "r2": 0.9967827391886754, + "mae": 0.010102579672034205 + }, + { + "threshold": 0.015, + "nz": 3, + "r2": 0.9967827391886754, + "mae": 0.010102579672034205 + }, + { + "threshold": 0.02, + "nz": 3, + "r2": 0.9967827391886754, + "mae": 0.010102579672034205 + }, + { + "threshold": 0.03, + "nz": 3, + "r2": 0.9967827391886754, + "mae": 0.010102579672034205 + }, + { + "threshold": 0.05, + "nz": 3, + "r2": 0.9967827391886754, + "mae": 0.010102579672034205 + }, + { + "threshold": 0.1, + "nz": 1, + "r2": 0.9213114343048161, + "mae": 0.05630183130409067 + } + ], + "best": { + "threshold": 0.0, + "nz": 21, + "r2": 0.998319769966323, + "mae": 0.007229121645243238 + }, + "best_coef": [ + -0.0031319599705785356, + 0.0009005213487458124, + 0.0021387422607032666, + -0.0005899816205869717, + 0.008569841953090828, + -0.009200996175780681, + 0.00613986870304347, + -0.0049747369106812895, + 0.0016847031130064957, + -0.0008914713562130392, + 0.009136994459848035, + -0.0015464725053784005, + 0.0002406988360957919, + 0.008618191191472493, + 3.5643410959938684e-05, + -0.0012826092968625834, + 0.5373930579297568, + 0.02251303312255063, + -0.01474954032019323, + 0.21424605058608626, + -0.12436843773164996 + ], + "sparsity_curve": [ + [ + 0.0, + 21, + 0.998319769966323 + ], + [ + 0.001, + 19, + 0.9983129034537462 + ], + [ + 0.002, + 18, + 0.9982861124400884 + ], + [ + 0.005, + 4, + 0.9974439573320747 + ], + [ + 0.01, + 3, + 0.9967827391886754 + ], + [ + 0.015, + 3, + 0.9967827391886754 + ], + [ + 0.02, + 3, + 0.9967827391886754 + ], + [ + 0.03, + 3, + 0.9967827391886754 + ], + [ + 0.05, + 3, + 0.9967827391886754 + ], + [ + 0.1, + 1, + 0.9213114343048161 + ] + ] + }, + "top": { + "results": [ + { + "threshold": 0.0, + "nz": 22, + "r2": 0.9893628989724597, + "mae": 0.008399062895981057 + }, + { + "threshold": 0.001, + "nz": 18, + "r2": 0.9893537060208671, + "mae": 0.008403249019293277 + }, + { + "threshold": 0.002, + "nz": 18, + "r2": 0.9893537060208671, + "mae": 0.008403249019293277 + }, + { + "threshold": 0.005, + "nz": 15, + "r2": 0.9887822320892092, + "mae": 0.008699231098152847 + }, + { + "threshold": 0.01, + "nz": 2, + "r2": 0.9434555237401117, + "mae": 0.020489945342854143 + }, + { + "threshold": 0.015, + "nz": 2, + "r2": 0.9434555237401117, + "mae": 0.020489945342854143 + }, + { + "threshold": 0.02, + "nz": 2, + "r2": 0.9434555237401117, + "mae": 0.020489945342854143 + }, + { + "threshold": 0.03, + "nz": 1, + "r2": 0.767474266747063, + "mae": 0.04368785439174068 + }, + { + "threshold": 0.05, + "nz": 1, + "r2": 0.767474266747063, + "mae": 0.04368785439174068 + }, + { + "threshold": 0.1, + "nz": 0, + "r2": -0.24002106699459924, + "mae": 0.09447869485375857 + } + ], + "best": { + "threshold": 0.0, + "nz": 22, + "r2": 0.9893628989724597, + "mae": 0.008399062895981057 + }, + "best_coef": [ + 0.009006304945500938, + -0.0013656895504682409, + 0.0009413896509799071, + -0.00039458277456129134, + -0.003513369230902717, + -0.01007127794832462, + 0.03628955219839411, + 0.005437814717321779, + -0.010370704341076153, + -6.79650067476857e-05, + 0.00024014904867861983, + -0.0003024802497459079, + -0.00022386382334619865, + 0.0060467393319888684, + -0.00012099892398619243, + 0.0024340238439430804, + 0.004483434102350851, + 0.00036025218585711294, + 0.023534741132533694, + -0.08783423089354239, + -0.25178195059980246, + -0.25926759753886264 + ], + "sparsity_curve": [ + [ + 0.0, + 22, + 0.9893628989724597 + ], + [ + 0.001, + 18, + 0.9893537060208671 + ], + [ + 0.002, + 18, + 0.9893537060208671 + ], + [ + 0.005, + 15, + 0.9887822320892092 + ], + [ + 0.01, + 2, + 0.9434555237401117 + ], + [ + 0.015, + 2, + 0.9434555237401117 + ], + [ + 0.02, + 2, + 0.9434555237401117 + ], + [ + 0.03, + 1, + 0.767474266747063 + ], + [ + 0.05, + 1, + 0.767474266747063 + ], + [ + 0.1, + 0, + -0.24002106699459924 + ] + ] + }, + "bottom": { + "results": [ + { + "threshold": 0.0, + "nz": 22, + "r2": 0.9964975415527493, + "mae": 0.0064379526638040155 + }, + { + "threshold": 0.001, + "nz": 20, + "r2": 0.996489370927338, + "mae": 0.00645906250002319 + }, + { + "threshold": 0.002, + "nz": 16, + "r2": 0.9964291093418592, + "mae": 0.0065655743861391825 + }, + { + "threshold": 0.005, + "nz": 10, + "r2": 0.9953098170863071, + "mae": 0.007493523579985216 + }, + { + "threshold": 0.01, + "nz": 4, + "r2": 0.9892509229731086, + "mae": 0.011711342942241802 + }, + { + "threshold": 0.015, + "nz": 3, + "r2": 0.9864981790968609, + "mae": 0.01350374385082598 + }, + { + "threshold": 0.02, + "nz": 2, + "r2": 0.9764723466980298, + "mae": 0.019381200759336845 + }, + { + "threshold": 0.03, + "nz": 1, + "r2": 0.8506889998403568, + "mae": 0.044906642626509306 + }, + { + "threshold": 0.05, + "nz": 1, + "r2": 0.8506889998403568, + "mae": 0.044906642626509306 + }, + { + "threshold": 0.1, + "nz": 0, + "r2": -0.1924891558656865, + "mae": 0.12604671778307872 + } + ], + "best": { + "threshold": 0.0, + "nz": 22, + "r2": 0.9964975415527493, + "mae": 0.0064379526638040155 + }, + "best_coef": [ + -0.05202015913990901, + 0.003985033852842102, + 0.0006739594919587617, + -0.002812076318050239, + 0.003286418720861537, + -0.0014206057573313623, + 0.0041256725776505215, + 0.010138751465280819, + 0.0035878309521334235, + 0.0003261963639967548, + 0.0005596011621300944, + -0.0005079578594815991, + 0.006109843511268079, + -0.00046788962018232675, + -0.003415058638294254, + 0.004795594046438008, + 0.001080165195723476, + -0.00208080634455238, + 0.016848987253660148, + 0.08216046763939658, + -0.03551514238315014, + 0.08969580568091838 + ], + "sparsity_curve": [ + [ + 0.0, + 22, + 0.9964975415527493 + ], + [ + 0.001, + 20, + 0.996489370927338 + ], + [ + 0.002, + 16, + 0.9964291093418592 + ], + [ + 0.005, + 10, + 0.9953098170863071 + ], + [ + 0.01, + 4, + 0.9892509229731086 + ], + [ + 0.015, + 3, + 0.9864981790968609 + ], + [ + 0.02, + 2, + 0.9764723466980298 + ], + [ + 0.03, + 1, + 0.8506889998403568 + ], + [ + 0.05, + 1, + 0.8506889998403568 + ], + [ + 0.1, + 0, + -0.1924891558656865 + ] + ] + } + }, + "karman_re100": { + "scene": "karman_re100", + "re_code": 100, + "mu": 0.02, + "n_samples": 198, + "n_features_front": 21, + "n_features_rear": 22, + "feature_names_front": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "sin_ua", + "cos_ua", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff" + ], + "feature_names_rear": [ + "bias", + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "sin_ua", + "cos_ua", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff" + ], + "front": { + "results": [ + { + "threshold": 0.0, + "nz": 21, + "r2": 0.9950013415086292, + "mae": 0.009297861947373046 + }, + { + "threshold": 0.001, + "nz": 18, + "r2": 0.9949963098276752, + "mae": 0.009274493189192809 + }, + { + "threshold": 0.002, + "nz": 13, + "r2": 0.9947660566975605, + "mae": 0.00982566021256978 + }, + { + "threshold": 0.005, + "nz": 12, + "r2": 0.9939890287801123, + "mae": 0.011077058192444884 + }, + { + "threshold": 0.01, + "nz": 6, + "r2": 0.9908017426802015, + "mae": 0.013580429645776177 + }, + { + "threshold": 0.015, + "nz": 4, + "r2": 0.989378747581318, + "mae": 0.014496650604934345 + }, + { + "threshold": 0.02, + "nz": 3, + "r2": 0.9692414821446799, + "mae": 0.02439129142750941 + }, + { + "threshold": 0.03, + "nz": 3, + "r2": 0.9692414821446799, + "mae": 0.02439129142750941 + }, + { + "threshold": 0.05, + "nz": 3, + "r2": 0.9692414821446799, + "mae": 0.02439129142750941 + }, + { + "threshold": 0.1, + "nz": 2, + "r2": 0.8681678005649112, + "mae": 0.05348411227986087 + } + ], + "best": { + "threshold": 0.0, + "nz": 21, + "r2": 0.9950013415086292, + "mae": 0.009297861947373046 + }, + "best_coef": [ + 0.002913453467073268, + -0.0010034473094889056, + -0.00015119435077007708, + 0.0012453743753331277, + 0.003746067771916422, + 0.012365721744701548, + -0.004422233294377436, + 0.016310385466661095, + -0.0003123427809473034, + 0.0004968569560360933, + 0.006918585044605247, + 0.0013427595425374512, + -0.003486854051080906, + 0.002723391394259924, + -0.0021322606391810995, + 0.0015689229718389174, + 0.9234972689284461, + -0.050172366027705036, + 0.0622687182995332, + 0.18730338573081926, + 0.8155192599082862 + ], + "sparsity_curve": [ + [ + 0.0, + 21, + 0.9950013415086292 + ], + [ + 0.001, + 18, + 0.9949963098276752 + ], + [ + 0.002, + 13, + 0.9947660566975605 + ], + [ + 0.005, + 12, + 0.9939890287801123 + ], + [ + 0.01, + 6, + 0.9908017426802015 + ], + [ + 0.015, + 4, + 0.989378747581318 + ], + [ + 0.02, + 3, + 0.9692414821446799 + ], + [ + 0.03, + 3, + 0.9692414821446799 + ], + [ + 0.05, + 3, + 0.9692414821446799 + ], + [ + 0.1, + 2, + 0.8681678005649112 + ] + ] + }, + "top": { + "results": [ + { + "threshold": 0.0, + "nz": 22, + "r2": 0.9928439394510484, + "mae": 0.014260238272228571 + }, + { + "threshold": 0.001, + "nz": 20, + "r2": 0.992824536423302, + "mae": 0.014349208218446834 + }, + { + "threshold": 0.002, + "nz": 17, + "r2": 0.9927843560231949, + "mae": 0.01439557962946284 + }, + { + "threshold": 0.005, + "nz": 17, + "r2": 0.9927843560231949, + "mae": 0.01439557962946284 + }, + { + "threshold": 0.01, + "nz": 12, + "r2": 0.9909377708950154, + "mae": 0.0157296791540154 + }, + { + "threshold": 0.015, + "nz": 12, + "r2": 0.9909377708950154, + "mae": 0.0157296791540154 + }, + { + "threshold": 0.02, + "nz": 12, + "r2": 0.9909377708950154, + "mae": 0.0157296791540154 + }, + { + "threshold": 0.03, + "nz": 6, + "r2": 0.9812946866555053, + "mae": 0.02411761595354287 + }, + { + "threshold": 0.05, + "nz": 2, + "r2": 0.9285296099294669, + "mae": 0.04588409913352275 + }, + { + "threshold": 0.1, + "nz": 1, + "r2": 0.44188145385324007, + "mae": 0.12621912517460543 + } + ], + "best": { + "threshold": 0.0, + "nz": 22, + "r2": 0.9928439394510484, + "mae": 0.014260238272228571 + }, + "best_coef": [ + 0.9660827819346605, + -0.008297125761372143, + -0.003911945137930959, + 0.0034814956807657227, + -0.0007517110088463216, + -0.02136581381381709, + 0.033416355828994854, + 0.09090888202182665, + 0.021342153859962566, + -0.0008885042212805717, + 0.0011163004543444928, + 0.003413122761270848, + 0.009655389486436414, + 0.0031436366855075283, + -0.00041107548453157643, + -0.003568754683600915, + 0.0051461956943505425, + 0.019321655644143756, + -0.19559725673163528, + -0.03758555104936961, + -1.0682906975825734, + 1.0671077959431223 + ], + "sparsity_curve": [ + [ + 0.0, + 22, + 0.9928439394510484 + ], + [ + 0.001, + 20, + 0.992824536423302 + ], + [ + 0.002, + 17, + 0.9927843560231949 + ], + [ + 0.005, + 17, + 0.9927843560231949 + ], + [ + 0.01, + 12, + 0.9909377708950154 + ], + [ + 0.015, + 12, + 0.9909377708950154 + ], + [ + 0.02, + 12, + 0.9909377708950154 + ], + [ + 0.03, + 6, + 0.9812946866555053 + ], + [ + 0.05, + 2, + 0.9285296099294669 + ], + [ + 0.1, + 1, + 0.44188145385324007 + ] + ] + }, + "bottom": { + "results": [ + { + "threshold": 0.0, + "nz": 22, + "r2": 0.9965335484991993, + "mae": 0.011523372809828718 + }, + { + "threshold": 0.001, + "nz": 18, + "r2": 0.9965311727162228, + "mae": 0.011494093354528386 + }, + { + "threshold": 0.002, + "nz": 16, + "r2": 0.9964335809851655, + "mae": 0.011697604600527748 + }, + { + "threshold": 0.005, + "nz": 13, + "r2": 0.9963877299663628, + "mae": 0.011836893357054774 + }, + { + "threshold": 0.01, + "nz": 10, + "r2": 0.9962551509064745, + "mae": 0.012131237037309098 + }, + { + "threshold": 0.015, + "nz": 10, + "r2": 0.9962551509064745, + "mae": 0.012131237037309098 + }, + { + "threshold": 0.02, + "nz": 8, + "r2": 0.9947803148283133, + "mae": 0.015270565294053379 + }, + { + "threshold": 0.03, + "nz": 8, + "r2": 0.9918438643725791, + "mae": 0.01736415070626163 + }, + { + "threshold": 0.05, + "nz": 2, + "r2": 0.9030386794798415, + "mae": 0.06608098355156587 + }, + { + "threshold": 0.1, + "nz": 1, + "r2": 0.8456588970543057, + "mae": 0.08685250875748249 + } + ], + "best": { + "threshold": 0.0, + "nz": 22, + "r2": 0.9965335484991993, + "mae": 0.011523372809828718 + }, + "best_coef": [ + -0.22933046011719774, + 0.00441915928491626, + -0.0020938732373856363, + -0.0029770176320210247, + 0.00021839723288964485, + -0.005410413891374701, + -0.016521988203524282, + 0.061471417818599695, + -0.00320616015626958, + -0.00022241266774999688, + 7.600088428371662e-05, + 0.0032437831416083558, + 0.00994611146006311, + 0.0019356852272298807, + -0.00287614144408484, + 0.004118949072765166, + -0.0026572329887406816, + -0.004586609213013399, + -0.10469366140552798, + 0.010919861645215943, + -0.2705206993407136, + -0.16030801380832282 + ], + "sparsity_curve": [ + [ + 0.0, + 22, + 0.9965335484991993 + ], + [ + 0.001, + 18, + 0.9965311727162228 + ], + [ + 0.002, + 16, + 0.9964335809851655 + ], + [ + 0.005, + 13, + 0.9963877299663628 + ], + [ + 0.01, + 10, + 0.9962551509064745 + ], + [ + 0.015, + 10, + 0.9962551509064745 + ], + [ + 0.02, + 8, + 0.9947803148283133 + ], + [ + 0.03, + 8, + 0.9918438643725791 + ], + [ + 0.05, + 2, + 0.9030386794798415 + ], + [ + 0.1, + 1, + 0.8456588970543057 + ] + ] + } + }, + "karman_re200": { + "scene": "karman_re200", + "re_code": 200, + "mu": 0.01, + "n_samples": 198, + "n_features_front": 21, + "n_features_rear": 22, + "feature_names_front": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "sin_ua", + "cos_ua", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff" + ], + "feature_names_rear": [ + "bias", + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "sin_ua", + "cos_ua", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff" + ], + "front": { + "results": [ + { + "threshold": 0.0, + "nz": 21, + "r2": 0.9565565952948164, + "mae": 0.048992270309083256 + }, + { + "threshold": 0.001, + "nz": 21, + "r2": 0.9565565952948164, + "mae": 0.048992270309083256 + }, + { + "threshold": 0.002, + "nz": 21, + "r2": 0.9565565952948164, + "mae": 0.048992270309083256 + }, + { + "threshold": 0.005, + "nz": 16, + "r2": 0.9556995055314772, + "mae": 0.049235757837804 + }, + { + "threshold": 0.01, + "nz": 13, + "r2": 0.9534652153648002, + "mae": 0.05019629912762377 + }, + { + "threshold": 0.015, + "nz": 12, + "r2": 0.9528134556742605, + "mae": 0.0506080965623989 + }, + { + "threshold": 0.02, + "nz": 10, + "r2": 0.9438381771460185, + "mae": 0.053768522266514816 + }, + { + "threshold": 0.03, + "nz": 6, + "r2": 0.9207971555743621, + "mae": 0.059392328951668336 + }, + { + "threshold": 0.05, + "nz": 3, + "r2": 0.8886657714941744, + "mae": 0.07146255905466381 + }, + { + "threshold": 0.1, + "nz": 3, + "r2": 0.8886657714941744, + "mae": 0.07146255905466381 + } + ], + "best": { + "threshold": 0.0, + "nz": 21, + "r2": 0.9565565952948164, + "mae": 0.048992270309083256 + }, + "best_coef": [ + 0.0025250328345394857, + -0.00030552517099191216, + -0.0005157197565304931, + -0.0009358937691519228, + 0.00811834023902148, + 0.004698768808610153, + -0.013179199045188592, + 0.008022774095407696, + 0.005748990258117735, + -0.014393040251094861, + 0.00404638728191461, + -0.0036851254015715326, + 0.0008965462342368713, + 0.007272101305605151, + 0.004468137310301827, + -0.0011281026248114966, + -2.0964314329520084, + -0.030552516911066304, + -0.09358937546373172, + 0.8118340246648522, + 0.8022774000432843 + ], + "sparsity_curve": [ + [ + 0.0, + 21, + 0.9565565952948164 + ], + [ + 0.001, + 21, + 0.9565565952948164 + ], + [ + 0.002, + 21, + 0.9565565952948164 + ], + [ + 0.005, + 16, + 0.9556995055314772 + ], + [ + 0.01, + 13, + 0.9534652153648002 + ], + [ + 0.015, + 12, + 0.9528134556742605 + ], + [ + 0.02, + 10, + 0.9438381771460185 + ], + [ + 0.03, + 6, + 0.9207971555743621 + ], + [ + 0.05, + 3, + 0.8886657714941744 + ], + [ + 0.1, + 3, + 0.8886657714941744 + ] + ] + }, + "top": { + "results": [ + { + "threshold": 0.0, + "nz": 22, + "r2": 0.9136716302531197, + "mae": 0.13271717446398762 + }, + { + "threshold": 0.001, + "nz": 22, + "r2": 0.9136716302531197, + "mae": 0.13271717446398762 + }, + { + "threshold": 0.002, + "nz": 22, + "r2": 0.9136716302531197, + "mae": 0.13271717446398762 + }, + { + "threshold": 0.005, + "nz": 21, + "r2": 0.9136497787734177, + "mae": 0.1326615832490538 + }, + { + "threshold": 0.01, + "nz": 18, + "r2": 0.9127667005882467, + "mae": 0.13294926009165733 + }, + { + "threshold": 0.015, + "nz": 16, + "r2": 0.9123943175152608, + "mae": 0.13340774414743775 + }, + { + "threshold": 0.02, + "nz": 15, + "r2": 0.9119635062655607, + "mae": 0.13360066993766545 + }, + { + "threshold": 0.03, + "nz": 14, + "r2": 0.9095049725410859, + "mae": 0.13476999297969008 + }, + { + "threshold": 0.05, + "nz": 11, + "r2": 0.8970840818843505, + "mae": 0.14441084992627343 + }, + { + "threshold": 0.1, + "nz": 7, + "r2": 0.8674848759406946, + "mae": 0.16355708965721857 + } + ], + "best": { + "threshold": 0.0, + "nz": 22, + "r2": 0.9136716302531197, + "mae": 0.13271717446398762 + }, + "best_coef": [ + 0.6033643953730624, + -0.013001318623264728, + -0.0006888231500980483, + 0.002963263039873873, + 0.002162667508855548, + -0.017487929496577476, + -0.007804043064953329, + 0.0622637004757959, + -0.0171539523606414, + 0.0036374475559951282, + 0.04268070514006053, + 0.008784345015259603, + 0.008202019029225465, + 0.0035048209369071123, + -0.013928400471627302, + -0.010661946445881886, + 0.004826772877718546, + 0.006033643934077788, + -0.06888231591094213, + 0.21626674897761955, + -1.7487929508371736, + -1.7153951478157978 + ], + "sparsity_curve": [ + [ + 0.0, + 22, + 0.9136716302531197 + ], + [ + 0.001, + 22, + 0.9136716302531197 + ], + [ + 0.002, + 22, + 0.9136716302531197 + ], + [ + 0.005, + 21, + 0.9136497787734177 + ], + [ + 0.01, + 18, + 0.9127667005882467 + ], + [ + 0.015, + 16, + 0.9123943175152608 + ], + [ + 0.02, + 15, + 0.9119635062655607 + ], + [ + 0.03, + 14, + 0.9095049725410859 + ], + [ + 0.05, + 11, + 0.8970840818843505 + ], + [ + 0.1, + 7, + 0.8674848759406946 + ] + ] + }, + "bottom": { + "results": [ + { + "threshold": 0.0, + "nz": 22, + "r2": 0.918041512638942, + "mae": 0.09546886302890771 + }, + { + "threshold": 0.001, + "nz": 22, + "r2": 0.918041512638942, + "mae": 0.09546886302890771 + }, + { + "threshold": 0.002, + "nz": 22, + "r2": 0.918041512638942, + "mae": 0.09546886302890771 + }, + { + "threshold": 0.005, + "nz": 20, + "r2": 0.9180326957867992, + "mae": 0.09546002257462356 + }, + { + "threshold": 0.01, + "nz": 16, + "r2": 0.9163833397981528, + "mae": 0.09655749047162396 + }, + { + "threshold": 0.015, + "nz": 13, + "r2": 0.9144139186225387, + "mae": 0.09821641028624538 + }, + { + "threshold": 0.02, + "nz": 13, + "r2": 0.9144139186225387, + "mae": 0.09821641028624538 + }, + { + "threshold": 0.03, + "nz": 8, + "r2": 0.9073317807110457, + "mae": 0.10069456239205128 + }, + { + "threshold": 0.05, + "nz": 8, + "r2": 0.9073317807110457, + "mae": 0.10069456239205128 + }, + { + "threshold": 0.1, + "nz": 3, + "r2": 0.8132853582236775, + "mae": 0.1444794079589489 + } + ], + "best": { + "threshold": 0.0, + "nz": 22, + "r2": 0.918041512638942, + "mae": 0.09546886302890771 + }, + "best_coef": [ + 0.5194681644420974, + -0.007268574871488442, + -0.0001382882015259102, + 0.0014122761737770733, + 0.0019941493569474024, + -0.02173278599186409, + 0.005402769951884052, + 0.039885891947331276, + -0.00479604352418292, + -0.007473801916727194, + 0.029612326854934444, + 0.006640105319541523, + 0.01211780817575797, + -0.001886710806821879, + -0.01013667114742123, + -0.004177246747561825, + 0.0009568252957939229, + 0.005194681632060589, + -0.013828820328458407, + 0.19941493407141364, + -2.1732785992241563, + -0.4796043074818968 + ], + "sparsity_curve": [ + [ + 0.0, + 22, + 0.918041512638942 + ], + [ + 0.001, + 22, + 0.918041512638942 + ], + [ + 0.002, + 22, + 0.918041512638942 + ], + [ + 0.005, + 20, + 0.9180326957867992 + ], + [ + 0.01, + 16, + 0.9163833397981528 + ], + [ + 0.015, + 13, + 0.9144139186225387 + ], + [ + 0.02, + 13, + 0.9144139186225387 + ], + [ + 0.03, + 8, + 0.9073317807110457 + ], + [ + 0.05, + 8, + 0.9073317807110457 + ], + [ + 0.1, + 3, + 0.8132853582236775 + ] + ] + } + }, + "karman_re400": { + "scene": "karman_re400", + "re_code": 400, + "mu": 0.005, + "n_samples": 148, + "n_features_front": 21, + "n_features_rear": 22, + "feature_names_front": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "sin_ua", + "cos_ua", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff" + ], + "feature_names_rear": [ + "bias", + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "sin_ua", + "cos_ua", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff" + ], + "front": { + "results": [ + { + "threshold": 0.0, + "nz": 21, + "r2": 0.9910766698736291, + "mae": 0.011629558855658013 + }, + { + "threshold": 0.001, + "nz": 19, + "r2": 0.9910498397066849, + "mae": 0.011571370408140063 + }, + { + "threshold": 0.002, + "nz": 16, + "r2": 0.9907584709220429, + "mae": 0.011474480357144053 + }, + { + "threshold": 0.005, + "nz": 10, + "r2": 0.9884902032543673, + "mae": 0.012859069934565787 + }, + { + "threshold": 0.01, + "nz": 8, + "r2": 0.9865445971795084, + "mae": 0.013656070741254706 + }, + { + "threshold": 0.015, + "nz": 4, + "r2": 0.9754219455809757, + "mae": 0.017727590048987045 + }, + { + "threshold": 0.02, + "nz": 4, + "r2": 0.9754219455809757, + "mae": 0.017727590048987045 + }, + { + "threshold": 0.03, + "nz": 3, + "r2": 0.9703483630041645, + "mae": 0.019892056694056134 + }, + { + "threshold": 0.05, + "nz": 2, + "r2": 0.8086045777820393, + "mae": 0.05911817904399702 + }, + { + "threshold": 0.1, + "nz": 1, + "r2": -1.1864322013721562e-07, + "mae": 0.13795475986425845 + } + ], + "best": { + "threshold": 0.0, + "nz": 21, + "r2": 0.9910766698736291, + "mae": 0.011629558855658013 + }, + "best_coef": [ + 0.00012236151003792048, + 0.0005527515683824865, + 0.0001884345591579067, + -0.00019884208585139524, + -0.003254090054098988, + 0.004501560609074065, + 0.004254394757178572, + 0.0010083415349592146, + -0.0011145408470664684, + -0.00027208919025581185, + 0.004903963122854036, + -0.0008667057347028575, + -0.0009156023532667489, + -0.002244069252286602, + -0.0007849054200991619, + -0.0006688340291467981, + -0.3783093474577987, + 0.11055031386114175, + -0.03976841722790901, + -0.6508180110262577, + 0.20166830270639363 + ], + "sparsity_curve": [ + [ + 0.0, + 21, + 0.9910766698736291 + ], + [ + 0.001, + 19, + 0.9910498397066849 + ], + [ + 0.002, + 16, + 0.9907584709220429 + ], + [ + 0.005, + 10, + 0.9884902032543673 + ], + [ + 0.01, + 8, + 0.9865445971795084 + ], + [ + 0.015, + 4, + 0.9754219455809757 + ], + [ + 0.02, + 4, + 0.9754219455809757 + ], + [ + 0.03, + 3, + 0.9703483630041645 + ], + [ + 0.05, + 2, + 0.8086045777820393 + ], + [ + 0.1, + 1, + -1.1864322013721562e-07 + ] + ] + }, + "top": { + "results": [ + { + "threshold": 0.0, + "nz": 22, + "r2": 0.9791617707213738, + "mae": 0.04494529906498399 + }, + { + "threshold": 0.001, + "nz": 21, + "r2": 0.979161770721373, + "mae": 0.044945299064320744 + }, + { + "threshold": 0.002, + "nz": 20, + "r2": 0.9791418532403751, + "mae": 0.0450021479471988 + }, + { + "threshold": 0.005, + "nz": 15, + "r2": 0.9788691394557797, + "mae": 0.04504818600796471 + }, + { + "threshold": 0.01, + "nz": 11, + "r2": 0.9778604380183816, + "mae": 0.04673322295463206 + }, + { + "threshold": 0.015, + "nz": 10, + "r2": 0.9749252195935764, + "mae": 0.048414314214308786 + }, + { + "threshold": 0.02, + "nz": 10, + "r2": 0.9749252195935764, + "mae": 0.048414314214308786 + }, + { + "threshold": 0.03, + "nz": 6, + "r2": 0.9696026027584206, + "mae": 0.05150605405202583 + }, + { + "threshold": 0.05, + "nz": 3, + "r2": 0.9550986794209745, + "mae": 0.06435430676226091 + }, + { + "threshold": 0.1, + "nz": 2, + "r2": 0.9259624571185212, + "mae": 0.0837515425066929 + } + ], + "best": { + "threshold": 0.0, + "nz": 22, + "r2": 0.9791617707213738, + "mae": 0.04494529906498399 + }, + "best_coef": [ + 0.08671233316981866, + -0.0014078616575270183, + -0.0011893114948202201, + -0.00027494239940701806, + 0.0005636347911606242, + 0.0018931610984057356, + 0.010079781145559281, + 0.002243578571231687, + -0.0018733090911482563, + 0.006358127163967422, + 0.0029488047574932914, + 0.012900969453655643, + 0.0018585854990862327, + 0.010593421792038704, + 0.0046178581381171825, + 0.001966279273844412, + 0.0025327701360673286, + 0.0004335616645658923, + -0.23786229795947839, + 0.11272695662098822, + 0.37863223658197603, + -0.37466179544216405 + ], + "sparsity_curve": [ + [ + 0.0, + 22, + 0.9791617707213738 + ], + [ + 0.001, + 21, + 0.979161770721373 + ], + [ + 0.002, + 20, + 0.9791418532403751 + ], + [ + 0.005, + 15, + 0.9788691394557797 + ], + [ + 0.01, + 11, + 0.9778604380183816 + ], + [ + 0.015, + 10, + 0.9749252195935764 + ], + [ + 0.02, + 10, + 0.9749252195935764 + ], + [ + 0.03, + 6, + 0.9696026027584206 + ], + [ + 0.05, + 3, + 0.9550986794209745 + ], + [ + 0.1, + 2, + 0.9259624571185212 + ] + ] + }, + "bottom": { + "results": [ + { + "threshold": 0.0, + "nz": 22, + "r2": 0.9686581410934367, + "mae": 0.04868261400704479 + }, + { + "threshold": 0.001, + "nz": 22, + "r2": 0.9686581410934367, + "mae": 0.04868261400704479 + }, + { + "threshold": 0.002, + "nz": 17, + "r2": 0.9685946535884615, + "mae": 0.04886158384662259 + }, + { + "threshold": 0.005, + "nz": 12, + "r2": 0.968134863052741, + "mae": 0.04937227005620415 + }, + { + "threshold": 0.01, + "nz": 11, + "r2": 0.9677608817383357, + "mae": 0.04973438224463947 + }, + { + "threshold": 0.015, + "nz": 10, + "r2": 0.9646891014423458, + "mae": 0.051622150117990234 + }, + { + "threshold": 0.02, + "nz": 7, + "r2": 0.9613283621164924, + "mae": 0.05383371646931655 + }, + { + "threshold": 0.03, + "nz": 7, + "r2": 0.9613283621164924, + "mae": 0.05383371646931655 + }, + { + "threshold": 0.05, + "nz": 5, + "r2": 0.9540490870907875, + "mae": 0.05855323697586902 + }, + { + "threshold": 0.1, + "nz": 4, + "r2": 0.9489267712729627, + "mae": 0.06030133862863786 + } + ], + "best": { + "threshold": 0.0, + "nz": 22, + "r2": 0.9686581410934367, + "mae": 0.04868261400704479 + }, + "best_coef": [ + 0.31963161225069453, + -0.0014417826804902915, + -0.0009488399798131276, + -0.00016301389284321925, + 0.00019367331652549443, + -0.0013907167791004245, + -0.0010719152858884842, + 0.009642492550627655, + 0.0005982386331633066, + 0.009400066467505037, + 0.005412675334329632, + 0.011942792611222953, + 0.006608645806628951, + 0.005356341917455254, + 0.002757244045268886, + 0.006597290057495822, + -0.0020024705507485736, + 0.0015981580589943245, + -0.18976799549284054, + 0.03873466257245657, + -0.2781433523877088, + 0.11964768680720898 + ], + "sparsity_curve": [ + [ + 0.0, + 22, + 0.9686581410934367 + ], + [ + 0.001, + 22, + 0.9686581410934367 + ], + [ + 0.002, + 17, + 0.9685946535884615 + ], + [ + 0.005, + 12, + 0.968134863052741 + ], + [ + 0.01, + 11, + 0.9677608817383357 + ], + [ + 0.015, + 10, + 0.9646891014423458 + ], + [ + 0.02, + 7, + 0.9613283621164924 + ], + [ + 0.03, + 7, + 0.9613283621164924 + ], + [ + 0.05, + 5, + 0.9540490870907875 + ], + [ + 0.1, + 4, + 0.9489267712729627 + ] + ] + } + } + } +} \ No newline at end of file diff --git a/src/SR_analysis/old_data/sindy_karman_with_mu.json b/src/SR_analysis/old_data/sindy_karman_with_mu.json new file mode 100644 index 0000000..18b6356 --- /dev/null +++ b/src/SR_analysis/old_data/sindy_karman_with_mu.json @@ -0,0 +1,618 @@ +{ + "thresholds": [ + 0.0, + 0.001, + 0.002, + 0.005, + 0.01, + 0.015, + 0.02, + 0.03, + 0.05, + 0.1 + ], + "feature_names_front": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "sin_ua", + "cos_ua", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff" + ], + "feature_names_rear": [ + "bias", + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "sin_ua", + "cos_ua", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff" + ], + "per_scene": { + "karman_re100": { + "scene": "karman_re100", + "re_code": 100, + "mu": 0.02, + "feature_names_front": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "sin_ua", + "cos_ua", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff" + ], + "feature_names_rear": [ + "bias", + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "sin_ua", + "cos_ua", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff" + ], + "front": { + "results": [ + { + "threshold": 0.0, + "nz": 21, + "r2": 0.9963468671522658, + "mae": 0.008495019589563084, + "weights_min": 0.2538872797382758, + "weights_max": 1.0371907088754313 + }, + { + "threshold": 0.001, + "nz": 18, + "r2": 0.9963454010383349, + "mae": 0.008500938429721638, + "weights_min": 0.2538872797382758, + "weights_max": 1.0371907088754313 + }, + { + "threshold": 0.002, + "nz": 18, + "r2": 0.9963454010383349, + "mae": 0.008500938429721638, + "weights_min": 0.2538872797382758, + "weights_max": 1.0371907088754313 + }, + { + "threshold": 0.005, + "nz": 12, + "r2": 0.9947081910813219, + "mae": 0.010856555791902431, + "weights_min": 0.2538872797382758, + "weights_max": 1.0371907088754313 + }, + { + "threshold": 0.01, + "nz": 9, + "r2": 0.9944015269767014, + "mae": 0.010728158860757616, + "weights_min": 0.2538872797382758, + "weights_max": 1.0371907088754313 + }, + { + "threshold": 0.015, + "nz": 4, + "r2": 0.9805810563712329, + "mae": 0.02109193834223528, + "weights_min": 0.2538872797382758, + "weights_max": 1.0371907088754313 + }, + { + "threshold": 0.02, + "nz": 3, + "r2": 0.969677527911751, + "mae": 0.024398378538262957, + "weights_min": 0.2538872797382758, + "weights_max": 1.0371907088754313 + }, + { + "threshold": 0.03, + "nz": 3, + "r2": 0.969677527911751, + "mae": 0.024398378538262957, + "weights_min": 0.2538872797382758, + "weights_max": 1.0371907088754313 + }, + { + "threshold": 0.05, + "nz": 3, + "r2": 0.969677527911751, + "mae": 0.024398378538262957, + "weights_min": 0.2538872797382758, + "weights_max": 1.0371907088754313 + }, + { + "threshold": 0.1, + "nz": 2, + "r2": 0.8671246137520265, + "mae": 0.0536574860231316, + "weights_min": 0.2538872797382758, + "weights_max": 1.0371907088754313 + } + ], + "best": { + "threshold": 0.002, + "nz": 18, + "r2": 0.9963454010383349, + "mae": 0.008500938429721638, + "weights_min": 0.2538872797382758, + "weights_max": 1.0371907088754313 + }, + "best_coef": [ + 0.003380802864171953, + -0.0008809022653997198, + 0.0, + 0.00208717851683782, + 0.009307953320832238, + 0.012225442968436223, + -0.007383343207562919, + 0.019594876551595624, + 0.0, + 0.0, + 0.0071310792485739126, + 0.0014536838339802387, + -0.004129923355485657, + 0.0014498851666906254, + -0.0027671395780320186, + 0.0021240428248806, + 0.9081582144711916, + -0.044045113030994884, + 0.10435892480150129, + 0.4653976700088291, + 0.979743839707262 + ], + "sparsity_curve": [ + [ + 0.0, + 21, + 0.9963468671522658 + ], + [ + 0.001, + 18, + 0.9963454010383349 + ], + [ + 0.002, + 18, + 0.9963454010383349 + ], + [ + 0.005, + 12, + 0.9947081910813219 + ], + [ + 0.01, + 9, + 0.9944015269767014 + ], + [ + 0.015, + 4, + 0.9805810563712329 + ], + [ + 0.02, + 3, + 0.969677527911751 + ], + [ + 0.03, + 3, + 0.969677527911751 + ], + [ + 0.05, + 3, + 0.969677527911751 + ], + [ + 0.1, + 2, + 0.8671246137520265 + ] + ] + }, + "top": { + "results": [ + { + "threshold": 0.0, + "nz": 22, + "r2": 0.9947943202493076, + "mae": 0.013882836661429927, + "weights_min": 0.36423882973791505, + "weights_max": 1.1067365435175973 + }, + { + "threshold": 0.001, + "nz": 20, + "r2": 0.9947724287185891, + "mae": 0.013998132434573892, + "weights_min": 0.36423882973791505, + "weights_max": 1.1067365435175973 + }, + { + "threshold": 0.002, + "nz": 18, + "r2": 0.9947437375025526, + "mae": 0.013973729698665602, + "weights_min": 0.36423882973791505, + "weights_max": 1.1067365435175973 + }, + { + "threshold": 0.005, + "nz": 18, + "r2": 0.9947437375025526, + "mae": 0.013973729698665602, + "weights_min": 0.36423882973791505, + "weights_max": 1.1067365435175973 + }, + { + "threshold": 0.01, + "nz": 12, + "r2": 0.9930561323691804, + "mae": 0.015350435484475112, + "weights_min": 0.36423882973791505, + "weights_max": 1.1067365435175973 + }, + { + "threshold": 0.015, + "nz": 12, + "r2": 0.9930561323691804, + "mae": 0.015350435484475112, + "weights_min": 0.36423882973791505, + "weights_max": 1.1067365435175973 + }, + { + "threshold": 0.02, + "nz": 12, + "r2": 0.9930561323691804, + "mae": 0.015350435484475112, + "weights_min": 0.36423882973791505, + "weights_max": 1.1067365435175973 + }, + { + "threshold": 0.03, + "nz": 6, + "r2": 0.9829012294927744, + "mae": 0.024013187353778373, + "weights_min": 0.36423882973791505, + "weights_max": 1.1067365435175973 + }, + { + "threshold": 0.05, + "nz": 2, + "r2": 0.9310887280966665, + "mae": 0.04519335614359589, + "weights_min": 0.36423882973791505, + "weights_max": 1.1067365435175973 + }, + { + "threshold": 0.1, + "nz": 1, + "r2": 0.42576849886018275, + "mae": 0.12579383759812146, + "weights_min": 0.36423882973791505, + "weights_max": 1.1067365435175973 + } + ], + "best": { + "threshold": 0.002, + "nz": 18, + "r2": 0.9947437375025526, + "mae": 0.013973729698665602, + "weights_min": 0.36423882973791505, + "weights_max": 1.1067365435175973 + }, + "best_coef": [ + 0.9115729610071152, + -0.006805683305581021, + -0.00407714864334097, + 0.0028603164071186993, + 0.0, + -0.020649798768545318, + 0.033591198632074694, + 0.09195198469780816, + 0.0232186348784368, + 0.0, + 0.0, + 0.0035421651717378612, + 0.009890204105631987, + 0.0028849152878799955, + -0.000946773567394545, + -0.00391306181767762, + 0.005116314559440586, + 0.018231459167452172, + -0.20385743262256592, + 0.0, + -1.0324899193623733, + 1.1609316346427214 + ], + "sparsity_curve": [ + [ + 0.0, + 22, + 0.9947943202493076 + ], + [ + 0.001, + 20, + 0.9947724287185891 + ], + [ + 0.002, + 18, + 0.9947437375025526 + ], + [ + 0.005, + 18, + 0.9947437375025526 + ], + [ + 0.01, + 12, + 0.9930561323691804 + ], + [ + 0.015, + 12, + 0.9930561323691804 + ], + [ + 0.02, + 12, + 0.9930561323691804 + ], + [ + 0.03, + 6, + 0.9829012294927744 + ], + [ + 0.05, + 2, + 0.9310887280966665 + ], + [ + 0.1, + 1, + 0.42576849886018275 + ] + ] + }, + "bottom": { + "results": [ + { + "threshold": 0.0, + "nz": 22, + "r2": 0.9974702578481651, + "mae": 0.011121305785433843, + "weights_min": 0.2863478575619962, + "weights_max": 1.0399071561860243 + }, + { + "threshold": 0.001, + "nz": 18, + "r2": 0.9974635520604825, + "mae": 0.011155185750820898, + "weights_min": 0.2863478575619962, + "weights_max": 1.0399071561860243 + }, + { + "threshold": 0.002, + "nz": 16, + "r2": 0.9974006411842374, + "mae": 0.011251386324348665, + "weights_min": 0.2863478575619962, + "weights_max": 1.0399071561860243 + }, + { + "threshold": 0.005, + "nz": 16, + "r2": 0.9974006411842374, + "mae": 0.011251386324348665, + "weights_min": 0.2863478575619962, + "weights_max": 1.0399071561860243 + }, + { + "threshold": 0.01, + "nz": 10, + "r2": 0.9971310266820465, + "mae": 0.011717975996893105, + "weights_min": 0.2863478575619962, + "weights_max": 1.0399071561860243 + }, + { + "threshold": 0.015, + "nz": 8, + "r2": 0.9955140894896422, + "mae": 0.01509793962669375, + "weights_min": 0.2863478575619962, + "weights_max": 1.0399071561860243 + }, + { + "threshold": 0.02, + "nz": 8, + "r2": 0.9955140894896422, + "mae": 0.01509793962669375, + "weights_min": 0.2863478575619962, + "weights_max": 1.0399071561860243 + }, + { + "threshold": 0.03, + "nz": 5, + "r2": 0.9920045223882177, + "mae": 0.01968881107737399, + "weights_min": 0.2863478575619962, + "weights_max": 1.0399071561860243 + }, + { + "threshold": 0.05, + "nz": 2, + "r2": 0.9027764309517692, + "mae": 0.06593190744450401, + "weights_min": 0.2863478575619962, + "weights_max": 1.0399071561860243 + }, + { + "threshold": 0.1, + "nz": 1, + "r2": 0.8435989700357883, + "mae": 0.08688057754133625, + "weights_min": 0.2863478575619962, + "weights_max": 1.0399071561860243 + } + ], + "best": { + "threshold": 0.002, + "nz": 16, + "r2": 0.9974006411842374, + "mae": 0.011251386324348665, + "weights_min": 0.2863478575619962, + "weights_max": 1.0399071561860243 + }, + "best_coef": [ + -0.3062392108066253, + 0.005083824376570689, + -0.0019100164756928636, + -0.0032417665760502293, + 0.0, + 0.0, + -0.016413610065012466, + 0.062146484641493534, + -0.005222530364610314, + 0.0, + 0.0, + 0.003517044018032268, + 0.00988827788555667, + 0.0023305542256132016, + -0.0015468136369196132, + 0.004824491806330017, + -0.0032570158106502616, + -0.00612478415924937, + -0.09550082351895658, + 0.0, + 0.0, + -0.26112655898206505 + ], + "sparsity_curve": [ + [ + 0.0, + 22, + 0.9974702578481651 + ], + [ + 0.001, + 18, + 0.9974635520604825 + ], + [ + 0.002, + 16, + 0.9974006411842374 + ], + [ + 0.005, + 16, + 0.9974006411842374 + ], + [ + 0.01, + 10, + 0.9971310266820465 + ], + [ + 0.015, + 8, + 0.9955140894896422 + ], + [ + 0.02, + 8, + 0.9955140894896422 + ], + [ + 0.03, + 5, + 0.9920045223882177 + ], + [ + 0.05, + 2, + 0.9027764309517692 + ], + [ + 0.1, + 1, + 0.8435989700357883 + ] + ] + } + } + } +} \ No newline at end of file diff --git a/src/SR_analysis/old_data/sindy_karman_with_sin.json b/src/SR_analysis/old_data/sindy_karman_with_sin.json new file mode 100644 index 0000000..01b0aaf --- /dev/null +++ b/src/SR_analysis/old_data/sindy_karman_with_sin.json @@ -0,0 +1,420 @@ +{ + "thresholds": [ + 0.0, + 0.001, + 0.002, + 0.005, + 0.01, + 0.015, + 0.02, + 0.03, + 0.05, + 0.1 + ], + "feature_names_front": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "sin_ua", + "cos_ua", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT" + ], + "feature_names_rear": [ + "bias", + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "sin_ua", + "cos_ua", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT" + ], + "per_scene": { + "karman_re100": { + "scene": "karman_re100", + "re_code": 100, + "mu": 0.02, + "feature_names_front": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "sin_ua", + "cos_ua", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT" + ], + "feature_names_rear": [ + "bias", + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "sin_ua", + "cos_ua", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT" + ], + "front": { + "results": [ + { + "threshold": 0.0, + "nz": 16, + "r2": 0.9964173297164212, + "mae": 0.008496979102955536, + "weights_min": 0.2778368062626606, + "weights_max": 1.037970618101812 + }, + { + "threshold": 0.001, + "nz": 13, + "r2": 0.9964170471154649, + "mae": 0.00849877674299794, + "weights_min": 0.2778368062626606, + "weights_max": 1.037970618101812 + }, + { + "threshold": 0.002, + "nz": 13, + "r2": 0.9964170471154649, + "mae": 0.00849877674299794, + "weights_min": 0.2778368062626606, + "weights_max": 1.037970618101812 + }, + { + "threshold": 0.005, + "nz": 3, + "r2": 0.9885903068210383, + "mae": 0.014540007299834038, + "weights_min": 0.2778368062626606, + "weights_max": 1.037970618101812 + }, + { + "threshold": 0.01, + "nz": 3, + "r2": 0.9801597431201753, + "mae": 0.02107344007147761, + "weights_min": 0.2778368062626606, + "weights_max": 1.037970618101812 + }, + { + "threshold": 0.015, + "nz": 3, + "r2": 0.9801597431201753, + "mae": 0.02107344007147761, + "weights_min": 0.2778368062626606, + "weights_max": 1.037970618101812 + }, + { + "threshold": 0.02, + "nz": 2, + "r2": 0.9680925119257522, + "mae": 0.024303837665639722, + "weights_min": 0.2778368062626606, + "weights_max": 1.037970618101812 + }, + { + "threshold": 0.03, + "nz": 2, + "r2": 0.9680925119257522, + "mae": 0.024303837665639722, + "weights_min": 0.2778368062626606, + "weights_max": 1.037970618101812 + }, + { + "threshold": 0.05, + "nz": 2, + "r2": 0.9680925119257522, + "mae": 0.024303837665639722, + "weights_min": 0.2778368062626606, + "weights_max": 1.037970618101812 + }, + { + "threshold": 0.1, + "nz": 1, + "r2": 0.8667444738394041, + "mae": 0.053551706949699907, + "weights_min": 0.2778368062626606, + "weights_max": 1.037970618101812 + } + ], + "best": { + "threshold": 0.002, + "nz": 13, + "r2": 0.9964170471154649, + "mae": 0.00849877674299794, + "weights_min": 0.2778368062626606, + "weights_max": 1.037970618101812 + }, + "best_coef": [ + 0.0035693198413540234, + -0.0018962945486085396, + 0.0, + 0.004246223224503542, + 0.01867909228324871, + 0.011809790151579671, + -0.006291860766813562, + 0.03938466562589553, + 0.0, + 0.0, + 0.0071131542686216345, + 0.0015118413457375216, + -0.004145051172507872, + 0.0014265611111992293, + -0.0028705905475650928, + 0.0021914873908442335 + ], + "sparsity_curve": [ + [ + 0.0, + 16, + 0.9964173297164212 + ], + [ + 0.001, + 13, + 0.9964170471154649 + ], + [ + 0.002, + 13, + 0.9964170471154649 + ], + [ + 0.005, + 3, + 0.9885903068210383 + ], + [ + 0.01, + 3, + 0.9801597431201753 + ], + [ + 0.015, + 3, + 0.9801597431201753 + ], + [ + 0.02, + 2, + 0.9680925119257522 + ], + [ + 0.03, + 2, + 0.9680925119257522 + ], + [ + 0.05, + 2, + 0.9680925119257522 + ], + [ + 0.1, + 1, + 0.8667444738394041 + ] + ] + }, + "top": { + "results": [ + { + "threshold": 0.0, + "nz": 17, + "r2": 0.9947943200325069, + "mae": 0.01388285452459937, + "weights_min": 0.3642388297379152, + "weights_max": 1.1067365435175973 + }, + { + "threshold": 0.001, + "nz": 15, + "r2": 0.9947724285032213, + "mae": 0.013998153842830187, + "weights_min": 0.3642388297379152, + "weights_max": 1.1067365435175973 + }, + { + "threshold": 0.002, + "nz": 15, + "r2": 0.9947724285032213, + "mae": 0.013998153842830187, + "weights_min": 0.3642388297379152, + "weights_max": 1.1067365435175973 + }, + { + "threshold": 0.005, + "nz": 14, + "r2": 0.9947437372987523, + "mae": 0.013973763318069576, + "weights_min": 0.3642388297379152, + "weights_max": 1.1067365435175973 + }, + { + "threshold": 0.01, + "nz": 11, + "r2": 0.994527427094902, + "mae": 0.014375559061365345, + "weights_min": 0.3642388297379152, + "weights_max": 1.1067365435175973 + }, + { + "threshold": 0.015, + "nz": 10, + "r2": 0.9930561323361152, + "mae": 0.015350427162982516, + "weights_min": 0.3642388297379152, + "weights_max": 1.1067365435175973 + }, + { + "threshold": 0.02, + "nz": 10, + "r2": 0.9930561323361152, + "mae": 0.015350427162982516, + "weights_min": 0.3642388297379152, + "weights_max": 1.1067365435175973 + }, + { + "threshold": 0.03, + "nz": 9, + "r2": 0.9923352206869693, + "mae": 0.016117400302495512, + "weights_min": 0.3642388297379152, + "weights_max": 1.1067365435175973 + }, + { + "threshold": 0.05, + "nz": 5, + "r2": 0.9832113360018212, + "mae": 0.02344478050006772, + "weights_min": 0.3642388297379152, + "weights_max": 1.1067365435175973 + }, + { + "threshold": 0.1, + "nz": 2, + "r2": 0.5190549772066559, + "mae": 0.1224813760307, + "weights_min": 0.3642388297379152, + "weights_max": 1.1067365435175973 + } + ], + "best": { + "threshold": 0.002, + "nz": 15, + "r2": 0.9947724285032213, + "mae": 0.013998153842830187, + "weights_min": 0.3642388297379152, + "weights_max": 1.1067365435175973 + }, + "best_coef": [ + 1.0021483455333764, + -0.008212536312529608, + -0.008095982403317927, + 0.0033883289805039874, + -0.0014027415625294862, + -0.043634070706600914, + 0.034508992736236595, + 0.09250796489630796, + 0.046370682051450104, + 0.0, + 0.0, + 0.0035461180448679397, + 0.009932878542230005, + 0.002841799103744343, + -0.0009874050179528864, + -0.0039603133924009494, + 0.0051583627341490485 + ], + "sparsity_curve": [ + [ + 0.0, + 17, + 0.9947943200325069 + ], + [ + 0.001, + 15, + 0.9947724285032213 + ], + [ + 0.002, + 15, + 0.9947724285032213 + ], + [ + 0.005, + 14, + 0.9947437372987523 + ], + [ + 0.01, + 11, + 0.994527427094902 + ], + [ + 0.015, + 10, + 0.9930561323361152 + ], + [ + 0.02, + 10, + 0.9930561323361152 + ], + [ + 0.03, + 9, + 0.9923352206869693 + ], + [ + 0.05, + 5, + 0.9832113360018212 + ], + [ + 0.1, + 2, + 0.5190549772066559 + ] + ] + } + } + } +} \ No newline at end of file diff --git a/src/SR_analysis/old_data/sindy_vortex_v1.json b/src/SR_analysis/old_data/sindy_vortex_v1.json new file mode 100644 index 0000000..864c72e --- /dev/null +++ b/src/SR_analysis/old_data/sindy_vortex_v1.json @@ -0,0 +1,996 @@ +{ + "thresholds": [ + 0.0, + 0.001, + 0.002, + 0.005, + 0.01, + 0.015, + 0.02, + 0.03, + 0.05, + 0.1 + ], + "per_scene": { + "vortex_lamb": { + "scene": "vortex_lamb", + "re_code": 100, + "mu": 0.02, + "n_samples": 148, + "feature_names_front": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "sin_ua", + "cos_ua", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff" + ], + "feature_names_rear": [ + "bias", + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "sin_ua", + "cos_ua", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff" + ], + "front": { + "results": [ + { + "threshold": 0.0, + "nz": 21, + "r2": 0.9035051679446613, + "mae": 0.05502827225008196 + }, + { + "threshold": 0.001, + "nz": 20, + "r2": 0.9035028044287374, + "mae": 0.054966606007031876 + }, + { + "threshold": 0.002, + "nz": 20, + "r2": 0.9035028044287374, + "mae": 0.054966606007031876 + }, + { + "threshold": 0.005, + "nz": 16, + "r2": 0.9017891237504362, + "mae": 0.053710513734883655 + }, + { + "threshold": 0.01, + "nz": 9, + "r2": 0.8985847902462778, + "mae": 0.054491226319598914 + }, + { + "threshold": 0.015, + "nz": 8, + "r2": 0.8957625139671737, + "mae": 0.05306021520916354 + }, + { + "threshold": 0.02, + "nz": 8, + "r2": 0.8957625139671737, + "mae": 0.05306021520916354 + }, + { + "threshold": 0.03, + "nz": 8, + "r2": 0.8957625139671737, + "mae": 0.05306021520916354 + }, + { + "threshold": 0.05, + "nz": 3, + "r2": 0.8536388609680176, + "mae": 0.048637995761707624 + }, + { + "threshold": 0.1, + "nz": 3, + "r2": 0.8536388609680176, + "mae": 0.048637995761707624 + } + ], + "best": { + "threshold": 0.0, + "nz": 21, + "r2": 0.9035051679446613, + "mae": 0.05502827225008196 + }, + "best_coef": [ + 0.0072499249196507284, + -0.013406647625693383, + -0.00038101504054493135, + -0.01577043426582744, + -0.021357793291808133, + 0.09538149016633536, + -0.17597430070013861, + 0.01105460767691196, + 0.013467531810937875, + -0.008447877114831191, + -0.009842767133851753, + 0.019209968175028087, + -0.036801729600496, + 0.0036358387699529284, + 0.006079481319894673, + 0.003342294121453103, + -0.2971343137386559, + -0.6703323813926846, + -0.7885217073102587, + -1.0678896759007668, + 0.5527301613931733 + ], + "sparsity_curve": [ + [ + 0.0, + 21, + 0.9035051679446613 + ], + [ + 0.001, + 20, + 0.9035028044287374 + ], + [ + 0.002, + 20, + 0.9035028044287374 + ], + [ + 0.005, + 16, + 0.9017891237504362 + ], + [ + 0.01, + 9, + 0.8985847902462778 + ], + [ + 0.015, + 8, + 0.8957625139671737 + ], + [ + 0.02, + 8, + 0.8957625139671737 + ], + [ + 0.03, + 8, + 0.8957625139671737 + ], + [ + 0.05, + 3, + 0.8536388609680176 + ], + [ + 0.1, + 3, + 0.8536388609680176 + ] + ] + }, + "top": { + "results": [ + { + "threshold": 0.0, + "nz": 22, + "r2": 0.979810012198325, + "mae": 0.009405479490894075 + }, + { + "threshold": 0.001, + "nz": 20, + "r2": 0.9797408009698567, + "mae": 0.009464924709826631 + }, + { + "threshold": 0.002, + "nz": 19, + "r2": 0.9796029724451923, + "mae": 0.009503048001896178 + }, + { + "threshold": 0.005, + "nz": 6, + "r2": 0.9752589669369754, + "mae": 0.009083428201966606 + }, + { + "threshold": 0.01, + "nz": 5, + "r2": 0.9685659511773673, + "mae": 0.01027038394304665 + }, + { + "threshold": 0.015, + "nz": 2, + "r2": 0.9602153300731789, + "mae": 0.012152564325363832 + }, + { + "threshold": 0.02, + "nz": 2, + "r2": 0.9602153300731789, + "mae": 0.012152564325363832 + }, + { + "threshold": 0.03, + "nz": 2, + "r2": 0.9602153300731789, + "mae": 0.012152564325363832 + }, + { + "threshold": 0.05, + "nz": 1, + "r2": 0.9262664550660489, + "mae": 0.013270022046144844 + }, + { + "threshold": 0.1, + "nz": 0, + "r2": -0.2926478447353347, + "mae": 0.10377883511859723 + } + ], + "best": { + "threshold": 0.0, + "nz": 22, + "r2": 0.979810012198325, + "mae": 0.009405479490894075 + }, + "best_coef": [ + 1.7326565320927485, + -0.021510750075876994, + 0.0007162636287450304, + 0.003690011764588065, + -0.001255780396435901, + 0.008069876064011276, + -0.027165643005235728, + 0.019732103606457083, + -0.007023240411992188, + -0.00413143280625655, + 0.0016423995129504244, + 0.0008079534031552331, + -0.005050487340309005, + 0.011582526033145867, + 0.0010455718346845003, + 0.0012590379370475096, + 0.0019097817325384912, + 0.0346531306245988, + 0.03581318435812541, + -0.06278853147020641, + 0.40349380438892807, + -0.3511614171629877 + ], + "sparsity_curve": [ + [ + 0.0, + 22, + 0.979810012198325 + ], + [ + 0.001, + 20, + 0.9797408009698567 + ], + [ + 0.002, + 19, + 0.9796029724451923 + ], + [ + 0.005, + 6, + 0.9752589669369754 + ], + [ + 0.01, + 5, + 0.9685659511773673 + ], + [ + 0.015, + 2, + 0.9602153300731789 + ], + [ + 0.02, + 2, + 0.9602153300731789 + ], + [ + 0.03, + 2, + 0.9602153300731789 + ], + [ + 0.05, + 1, + 0.9262664550660489 + ], + [ + 0.1, + 0, + -0.2926478447353347 + ] + ] + }, + "bottom": { + "results": [ + { + "threshold": 0.0, + "nz": 22, + "r2": 0.9334422885170565, + "mae": 0.0062053698726041735 + }, + { + "threshold": 0.001, + "nz": 15, + "r2": 0.9318911728011019, + "mae": 0.00612106433470266 + }, + { + "threshold": 0.002, + "nz": 11, + "r2": 0.9294742132351927, + "mae": 0.006263040564511156 + }, + { + "threshold": 0.005, + "nz": 7, + "r2": 0.8963211646824344, + "mae": 0.006779203944819282 + }, + { + "threshold": 0.01, + "nz": 5, + "r2": 0.8721441125489685, + "mae": 0.007291293800356046 + }, + { + "threshold": 0.015, + "nz": 1, + "r2": 0.6935730348615337, + "mae": 0.009771975563999018 + }, + { + "threshold": 0.02, + "nz": 1, + "r2": 0.6935730348615337, + "mae": 0.009771975563999018 + }, + { + "threshold": 0.03, + "nz": 1, + "r2": 3.300804074513053e-12, + "mae": 0.029445380091027484 + }, + { + "threshold": 0.05, + "nz": 1, + "r2": 3.300804074513053e-12, + "mae": 0.029445380091027484 + }, + { + "threshold": 0.1, + "nz": 0, + "r2": -5.4349952892154265, + "mae": 0.0796928089615461 + } + ], + "best": { + "threshold": 0.0, + "nz": 22, + "r2": 0.9334422885170565, + "mae": 0.0062053698726041735 + }, + "best_coef": [ + 0.6365299396095444, + -0.00680014903595817, + -0.0014763845224131282, + 0.0008331648138976364, + -0.0013963333722837737, + 0.0026865900945637236, + -0.014425589645304323, + -0.008472370593784707, + -6.004332296000475e-05, + -0.0014926889561804521, + -0.0004258139447154254, + -0.0018644416734531306, + 0.006364757842372448, + -0.003199874412701356, + 0.001370932025212518, + 0.002115447057480394, + 0.0019992859663057654, + 0.012730598783987558, + -0.07381922521108229, + -0.069816526655473, + 0.13432950141437106, + -0.0030019765287261236 + ], + "sparsity_curve": [ + [ + 0.0, + 22, + 0.9334422885170565 + ], + [ + 0.001, + 15, + 0.9318911728011019 + ], + [ + 0.002, + 11, + 0.9294742132351927 + ], + [ + 0.005, + 7, + 0.8963211646824344 + ], + [ + 0.01, + 5, + 0.8721441125489685 + ], + [ + 0.015, + 1, + 0.6935730348615337 + ], + [ + 0.02, + 1, + 0.6935730348615337 + ], + [ + 0.03, + 1, + 3.300804074513053e-12 + ], + [ + 0.05, + 1, + 3.300804074513053e-12 + ], + [ + 0.1, + 0, + -5.4349952892154265 + ] + ] + } + }, + "vortex_taylor": { + "scene": "vortex_taylor", + "re_code": 100, + "mu": 0.02, + "n_samples": 148, + "feature_names_front": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "sin_ua", + "cos_ua", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff" + ], + "feature_names_rear": [ + "bias", + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "sin_ua", + "cos_ua", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff" + ], + "front": { + "results": [ + { + "threshold": 0.0, + "nz": 21, + "r2": 0.9603622630700551, + "mae": 5.5722956333111334e-05 + }, + { + "threshold": 0.001, + "nz": 0, + "r2": -1762.7026716770156, + "mae": 0.019129430850011273 + }, + { + "threshold": 0.002, + "nz": 0, + "r2": -1762.7026716770156, + "mae": 0.019129430850011273 + }, + { + "threshold": 0.005, + "nz": 0, + "r2": -1762.7026716770156, + "mae": 0.019129430850011273 + }, + { + "threshold": 0.01, + "nz": 0, + "r2": -1762.7026716770156, + "mae": 0.019129430850011273 + }, + { + "threshold": 0.015, + "nz": 0, + "r2": -1762.7026716770156, + "mae": 0.019129430850011273 + }, + { + "threshold": 0.02, + "nz": 0, + "r2": -1762.7026716770156, + "mae": 0.019129430850011273 + }, + { + "threshold": 0.03, + "nz": 0, + "r2": -1762.7026716770156, + "mae": 0.019129430850011273 + }, + { + "threshold": 0.05, + "nz": 0, + "r2": -1762.7026716770156, + "mae": 0.019129430850011273 + }, + { + "threshold": 0.1, + "nz": 0, + "r2": -1762.7026716770156, + "mae": 0.019129430850011273 + } + ], + "best": { + "threshold": 0.0, + "nz": 21, + "r2": 0.9603622630700551, + "mae": 5.5722956333111334e-05 + }, + "best_coef": [ + 0.00013185241334928363, + 0.0013869486093058166, + -2.0689454061431898e-05, + 0.0003650495654675631, + -7.646260510377518e-05, + 0.0016834710794748663, + 0.002138870811614627, + 0.0009672545588015969, + -0.0011958663539923654, + -0.00015017143903531285, + 0.007403929744140764, + -0.0015484862269750206, + -0.0003240821985992223, + -0.0010978795962499714, + -0.0002774795287867582, + -0.00041159658606479107, + 0.00011190538695352297, + 0.06934742736821846, + 0.018252511507361284, + -0.0038231311262574906, + 0.04836265570630274 + ], + "sparsity_curve": [ + [ + 0.0, + 21, + 0.9603622630700551 + ], + [ + 0.001, + 0, + -1762.7026716770156 + ], + [ + 0.002, + 0, + -1762.7026716770156 + ], + [ + 0.005, + 0, + -1762.7026716770156 + ], + [ + 0.01, + 0, + -1762.7026716770156 + ], + [ + 0.015, + 0, + -1762.7026716770156 + ], + [ + 0.02, + 0, + -1762.7026716770156 + ], + [ + 0.03, + 0, + -1762.7026716770156 + ], + [ + 0.05, + 0, + -1762.7026716770156 + ], + [ + 0.1, + 0, + -1762.7026716770156 + ] + ] + }, + "top": { + "results": [ + { + "threshold": 0.0, + "nz": 22, + "r2": 0.809824114603052, + "mae": 0.00019280734797529785 + }, + { + "threshold": 0.001, + "nz": 1, + "r2": 4.909409545561516e-09, + "mae": 0.0004627442799109909 + }, + { + "threshold": 0.002, + "nz": 1, + "r2": 4.909409545561516e-09, + "mae": 0.0004627442799109909 + }, + { + "threshold": 0.005, + "nz": 0, + "r2": -3813.3981416722418, + "mae": 0.06224470555379584 + }, + { + "threshold": 0.01, + "nz": 0, + "r2": -3813.3981416722418, + "mae": 0.06224470555379584 + }, + { + "threshold": 0.015, + "nz": 0, + "r2": -3813.3981416722418, + "mae": 0.06224470555379584 + }, + { + "threshold": 0.02, + "nz": 0, + "r2": -3813.3981416722418, + "mae": 0.06224470555379584 + }, + { + "threshold": 0.03, + "nz": 0, + "r2": -3813.3981416722418, + "mae": 0.06224470555379584 + }, + { + "threshold": 0.05, + "nz": 0, + "r2": -3813.3981416722418, + "mae": 0.06224470555379584 + }, + { + "threshold": 0.1, + "nz": 0, + "r2": -3813.3981416722418, + "mae": 0.06224470555379584 + } + ], + "best": { + "threshold": 0.0, + "nz": 22, + "r2": 0.809824114603052, + "mae": 0.00019280734797529785 + }, + "best_coef": [ + -0.003855648809544881, + -0.00016402917649747364, + -0.00930469925984968, + 0.0003725286141146794, + -0.0018143741612594137, + -0.0006228672933056743, + -0.004123657830294623, + 0.009617379353839065, + -0.0020693580870050163, + 0.0064472830467429834, + -0.0009363904689749538, + 0.002848747579355727, + 0.008718629599353982, + 0.006611618826853361, + 0.0026442625498493562, + -0.000822158875902874, + -0.00019526846815097482, + -7.71129751901901e-05, + -0.4652349584088788, + -0.09071890000133181, + -0.031143453311218216, + -0.10346632929440941 + ], + "sparsity_curve": [ + [ + 0.0, + 22, + 0.809824114603052 + ], + [ + 0.001, + 1, + 4.909409545561516e-09 + ], + [ + 0.002, + 1, + 4.909409545561516e-09 + ], + [ + 0.005, + 0, + -3813.3981416722418 + ], + [ + 0.01, + 0, + -3813.3981416722418 + ], + [ + 0.015, + 0, + -3813.3981416722418 + ], + [ + 0.02, + 0, + -3813.3981416722418 + ], + [ + 0.03, + 0, + -3813.3981416722418 + ], + [ + 0.05, + 0, + -3813.3981416722418 + ], + [ + 0.1, + 0, + -3813.3981416722418 + ] + ] + }, + "bottom": { + "results": [ + { + "threshold": 0.0, + "nz": 22, + "r2": 0.6431303693566448, + "mae": 0.00012115305583366485 + }, + { + "threshold": 0.001, + "nz": 1, + "r2": 2.5346330034814457e-08, + "mae": 0.00029202565622359403 + }, + { + "threshold": 0.002, + "nz": 1, + "r2": 2.5346330034814457e-08, + "mae": 0.00029202565622359403 + }, + { + "threshold": 0.005, + "nz": 1, + "r2": 2.5346330034814457e-08, + "mae": 0.00029202565622359403 + }, + { + "threshold": 0.01, + "nz": 1, + "r2": 2.5346330034814457e-08, + "mae": 0.00029202565622359403 + }, + { + "threshold": 0.015, + "nz": 1, + "r2": 2.5346330034814457e-08, + "mae": 0.00029202565622359403 + }, + { + "threshold": 0.02, + "nz": 1, + "r2": 2.5346330034814457e-08, + "mae": 0.00029202565622359403 + }, + { + "threshold": 0.03, + "nz": 0, + "r2": -11389.175969107222, + "mae": 0.05019249195686063 + }, + { + "threshold": 0.05, + "nz": 0, + "r2": -11389.175969107222, + "mae": 0.05019249195686063 + }, + { + "threshold": 0.1, + "nz": 0, + "r2": -11389.175969107222, + "mae": 0.05019249195686063 + } + ], + "best": { + "threshold": 0.0, + "nz": 22, + "r2": 0.6431303693566448, + "mae": 0.00012115305583366485 + }, + "best_coef": [ + 0.024327554221764875, + -0.0005249278989068122, + -0.0012993577286068104, + 2.2680966036791235e-07, + -0.00010725143675978186, + -6.83864821520398e-05, + 0.0010096871646746155, + 0.010716542986185212, + 0.0009908500661735154, + 0.0011374615793514392, + 0.0001758268322788275, + -0.0008997304730805852, + -0.0002053415257431248, + 0.0003868365622255378, + 0.005272105474899691, + 0.0033010584355117824, + 0.0027208004849191268, + 0.0004865510823514911, + -0.06496786884052097, + -0.005364174292120229, + -0.003419346453559459, + 0.04954205911186436 + ], + "sparsity_curve": [ + [ + 0.0, + 22, + 0.6431303693566448 + ], + [ + 0.001, + 1, + 2.5346330034814457e-08 + ], + [ + 0.002, + 1, + 2.5346330034814457e-08 + ], + [ + 0.005, + 1, + 2.5346330034814457e-08 + ], + [ + 0.01, + 1, + 2.5346330034814457e-08 + ], + [ + 0.015, + 1, + 2.5346330034814457e-08 + ], + [ + 0.02, + 1, + 2.5346330034814457e-08 + ], + [ + 0.03, + 0, + -11389.175969107222 + ], + [ + 0.05, + 0, + -11389.175969107222 + ], + [ + 0.1, + 0, + -11389.175969107222 + ] + ] + } + } + } +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/illusion_0.5L_pysr.json b/src/SR_analysis/results/archive/raw/illusion_0.5L_pysr.json new file mode 100644 index 0000000..a387051 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/illusion_0.5L_pysr.json @@ -0,0 +1,8 @@ +{ + "scene": "illusion_0.5L", + "mode": "pysr", + "similarity_full": 0.8539735529506018, + "similarity_tail": 0.8411474326454078, + "action_range": 0.06714382640888465, + "n_steps": 320 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/illusion_0.6L_pysr.json b/src/SR_analysis/results/archive/raw/illusion_0.6L_pysr.json new file mode 100644 index 0000000..1951547 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/illusion_0.6L_pysr.json @@ -0,0 +1,8 @@ +{ + "scene": "illusion_0.6L", + "mode": "pysr", + "similarity_full": 0.9388972369369757, + "similarity_tail": 0.8608654259522963, + "action_range": 0.044579733956375364, + "n_steps": 320 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/illusion_0.75L_pysr.json b/src/SR_analysis/results/archive/raw/illusion_0.75L_pysr.json new file mode 100644 index 0000000..d34259f --- /dev/null +++ b/src/SR_analysis/results/archive/raw/illusion_0.75L_pysr.json @@ -0,0 +1,8 @@ +{ + "scene": "illusion_0.75L", + "mode": "pysr", + "similarity_full": 0.9821949471763758, + "similarity_tail": 0.8071620435178003, + "action_range": 0.029841950903943048, + "n_steps": 320 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/illusion_0.75L_v23.json b/src/SR_analysis/results/archive/raw/illusion_0.75L_v23.json new file mode 100644 index 0000000..1205a89 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/illusion_0.75L_v23.json @@ -0,0 +1,9 @@ +{ + "scene": "illusion_0.75L", + "mode": "v23", + "similarity_full": 0.9741372518705987, + "similarity_tail": 0.9726050667188786, + "action_range": 0.03237645857182351, + "n_steps": 320, + "threshold": "best" +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/illusion_0.8L_pysr.json b/src/SR_analysis/results/archive/raw/illusion_0.8L_pysr.json new file mode 100644 index 0000000..6feb0d9 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/illusion_0.8L_pysr.json @@ -0,0 +1,8 @@ +{ + "scene": "illusion_0.8L", + "mode": "pysr", + "similarity_full": 0.9076366235277858, + "similarity_tail": 0.9003715798241534, + "action_range": 0.02705657196270742, + "n_steps": 214 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/illusion_1.2L_pysr.json b/src/SR_analysis/results/archive/raw/illusion_1.2L_pysr.json new file mode 100644 index 0000000..577669a --- /dev/null +++ b/src/SR_analysis/results/archive/raw/illusion_1.2L_pysr.json @@ -0,0 +1,8 @@ +{ + "scene": "illusion_1.2L", + "mode": "pysr", + "similarity_full": 0.8485137980740682, + "similarity_tail": 0.7692440834631539, + "action_range": 0.026140261986299245, + "n_steps": 214 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/illusion_1.5L_v23.json b/src/SR_analysis/results/archive/raw/illusion_1.5L_v23.json new file mode 100644 index 0000000..b9db40a --- /dev/null +++ b/src/SR_analysis/results/archive/raw/illusion_1.5L_v23.json @@ -0,0 +1,9 @@ +{ + "scene": "illusion_1.5L", + "mode": "v23", + "similarity_full": 0.9296044171184163, + "similarity_tail": 0.925839316423258, + "action_range": 0.035437546689072105, + "n_steps": 200, + "threshold": "best" +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/illusion_1L_pysr.json b/src/SR_analysis/results/archive/raw/illusion_1L_pysr.json new file mode 100644 index 0000000..b0d14e4 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/illusion_1L_pysr.json @@ -0,0 +1,8 @@ +{ + "scene": "illusion_1L", + "mode": "pysr", + "similarity_full": 0.9580609580562278, + "similarity_tail": 0.8857815231916633, + "action_range": 0.011634223590664046, + "n_steps": 214 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/illusion_1L_target_pysr.json b/src/SR_analysis/results/archive/raw/illusion_1L_target_pysr.json new file mode 100644 index 0000000..b0d14e4 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/illusion_1L_target_pysr.json @@ -0,0 +1,8 @@ +{ + "scene": "illusion_1L", + "mode": "pysr", + "similarity_full": 0.9580609580562278, + "similarity_tail": 0.8857815231916633, + "action_range": 0.011634223590664046, + "n_steps": 214 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/illusion_1L_v23.json b/src/SR_analysis/results/archive/raw/illusion_1L_v23.json new file mode 100644 index 0000000..c17671a --- /dev/null +++ b/src/SR_analysis/results/archive/raw/illusion_1L_v23.json @@ -0,0 +1,9 @@ +{ + "scene": "illusion_1L", + "mode": "v23", + "similarity_full": 0.9581664533499054, + "similarity_tail": 0.9575937863460072, + "action_range": 0.014234293670783398, + "n_steps": 320, + "threshold": "best" +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/illusion_2L_pysr.json b/src/SR_analysis/results/archive/raw/illusion_2L_pysr.json new file mode 100644 index 0000000..dfa58aa --- /dev/null +++ b/src/SR_analysis/results/archive/raw/illusion_2L_pysr.json @@ -0,0 +1,8 @@ +{ + "scene": "illusion_2L", + "mode": "pysr", + "similarity_full": 0.6754607445898861, + "similarity_tail": 0.6292225479264744, + "action_range": 0.3686004659586289, + "n_steps": 160 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/karman_joint_deep_front.json b/src/SR_analysis/results/archive/raw/karman_joint_deep_front.json new file mode 100644 index 0000000..c95321e --- /dev/null +++ b/src/SR_analysis/results/archive/raw/karman_joint_deep_front.json @@ -0,0 +1,27 @@ +{ + "scene": "karman_joint", + "scene_id": "karman", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "daF_dt", + "daB_dt", + "daT_dt", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "best_sympy": "daB_dt*0.38504666 + daF_dt + mu_Cl_tot*(-14.951645)", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/karman_joint_deep_top.json b/src/SR_analysis/results/archive/raw/karman_joint_deep_top.json new file mode 100644 index 0000000..fe18da8 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/karman_joint_deep_top.json @@ -0,0 +1,28 @@ +{ + "scene": "karman_joint", + "scene_id": "karman", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "daF_dt", + "daB_dt", + "daT_dt", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "best_sympy": "3.4138296", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/karman_re100_abs.json b/src/SR_analysis/results/archive/raw/karman_re100_abs.json new file mode 100644 index 0000000..f40b3ed --- /dev/null +++ b/src/SR_analysis/results/archive/raw/karman_re100_abs.json @@ -0,0 +1,7 @@ +{ + "scene": "karman_re100", + "mode": "abs", + "similarity": 0.7001671047053404, + "action_range": 0.8198985280719374, + "n_steps": 200 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/karman_re100_deriv.json b/src/SR_analysis/results/archive/raw/karman_re100_deriv.json new file mode 100644 index 0000000..b9ddeab --- /dev/null +++ b/src/SR_analysis/results/archive/raw/karman_re100_deriv.json @@ -0,0 +1,7 @@ +{ + "scene": "karman_re100", + "mode": "deriv", + "similarity": 0.6558668109381365, + "action_range": 0.4967922429092896, + "n_steps": 200 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/karman_re100_mu_pysr.json b/src/SR_analysis/results/archive/raw/karman_re100_mu_pysr.json new file mode 100644 index 0000000..7a38caf --- /dev/null +++ b/src/SR_analysis/results/archive/raw/karman_re100_mu_pysr.json @@ -0,0 +1,7 @@ +{ + "scene": "karman_re100", + "mode": "pysr", + "similarity": 0.8801077359149027, + "action_range": 0.05967765871961398, + "n_steps": 200 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/karman_re100_phase_pysr.json b/src/SR_analysis/results/archive/raw/karman_re100_phase_pysr.json new file mode 100644 index 0000000..7791860 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/karman_re100_phase_pysr.json @@ -0,0 +1,7 @@ +{ + "scene": "karman_re100", + "mode": "pysr", + "similarity": 0.888406234100047, + "action_range": 0.0578484540630116, + "n_steps": 200 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/karman_re100_pysr.json b/src/SR_analysis/results/archive/raw/karman_re100_pysr.json new file mode 100644 index 0000000..4a42bdb --- /dev/null +++ b/src/SR_analysis/results/archive/raw/karman_re100_pysr.json @@ -0,0 +1,7 @@ +{ + "scene": "karman_re100", + "mode": "pysr", + "similarity": 0.8876964046102431, + "action_range": 0.034138296000000005, + "n_steps": 200 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/karman_re100_v23_th0.003.json b/src/SR_analysis/results/archive/raw/karman_re100_v23_th0.003.json new file mode 100644 index 0000000..f11d2da --- /dev/null +++ b/src/SR_analysis/results/archive/raw/karman_re100_v23_th0.003.json @@ -0,0 +1,7 @@ +{ + "scene": "karman_re100", + "mode": "v23", + "similarity": 0.9010299497208456, + "action_range": 0.41788907540324977, + "n_steps": 200 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/karman_re150_v23.json b/src/SR_analysis/results/archive/raw/karman_re150_v23.json new file mode 100644 index 0000000..f2020eb --- /dev/null +++ b/src/SR_analysis/results/archive/raw/karman_re150_v23.json @@ -0,0 +1,7 @@ +{ + "scene": "karman_re150", + "mode": "v23", + "similarity": 0.5949698363534278, + "action_range": 0.2098074207020967, + "n_steps": 200 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/karman_re200_pysr.json b/src/SR_analysis/results/archive/raw/karman_re200_pysr.json new file mode 100644 index 0000000..42119cc --- /dev/null +++ b/src/SR_analysis/results/archive/raw/karman_re200_pysr.json @@ -0,0 +1,7 @@ +{ + "scene": "karman_re200", + "mode": "pysr", + "similarity": 0.8448181642866177, + "action_range": 0.034138296000000005, + "n_steps": 200 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/karman_re200_v23.json b/src/SR_analysis/results/archive/raw/karman_re200_v23.json new file mode 100644 index 0000000..1141830 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/karman_re200_v23.json @@ -0,0 +1,7 @@ +{ + "scene": "karman_re200", + "mode": "v23", + "similarity": 0.7930999192849009, + "action_range": 0.36164324360459554, + "n_steps": 200 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/karman_re25_v23.json b/src/SR_analysis/results/archive/raw/karman_re25_v23.json new file mode 100644 index 0000000..6cc9605 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/karman_re25_v23.json @@ -0,0 +1,7 @@ +{ + "scene": "karman_re25", + "mode": "v23", + "similarity": 0.5670501132177096, + "action_range": 0.386813219532425, + "n_steps": 200 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/karman_re300_v23.json b/src/SR_analysis/results/archive/raw/karman_re300_v23.json new file mode 100644 index 0000000..d30b67e --- /dev/null +++ b/src/SR_analysis/results/archive/raw/karman_re300_v23.json @@ -0,0 +1,7 @@ +{ + "scene": "karman_re300", + "mode": "v23", + "similarity": 0.5404784578855873, + "action_range": 0.1695910099653411, + "n_steps": 200 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/karman_re400_pysr.json b/src/SR_analysis/results/archive/raw/karman_re400_pysr.json new file mode 100644 index 0000000..0375b48 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/karman_re400_pysr.json @@ -0,0 +1,7 @@ +{ + "scene": "karman_re400", + "mode": "pysr", + "similarity": 0.8058735756048312, + "action_range": 0.034138296000000005, + "n_steps": 200 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/karman_re400_pysr_SI200.json b/src/SR_analysis/results/archive/raw/karman_re400_pysr_SI200.json new file mode 100644 index 0000000..20fa5c0 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/karman_re400_pysr_SI200.json @@ -0,0 +1,7 @@ +{ + "scene": "karman_re400", + "mode": "pysr", + "similarity": 0.7944157193756028, + "action_range": 0.034138296000000005, + "n_steps": 640 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/karman_re400_pysr_SI400.json b/src/SR_analysis/results/archive/raw/karman_re400_pysr_SI400.json new file mode 100644 index 0000000..9f70d25 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/karman_re400_pysr_SI400.json @@ -0,0 +1,7 @@ +{ + "scene": "karman_re400", + "mode": "pysr", + "similarity": 0.8185568187903199, + "action_range": 0.06124516065871972, + "n_steps": 320 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/karman_re400_v23.json b/src/SR_analysis/results/archive/raw/karman_re400_v23.json new file mode 100644 index 0000000..862b0c6 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/karman_re400_v23.json @@ -0,0 +1,7 @@ +{ + "scene": "karman_re400", + "mode": "v23", + "similarity": 0.6642044711806294, + "action_range": 0.17077547191291154, + "n_steps": 200 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/karman_re50_pysr.json b/src/SR_analysis/results/archive/raw/karman_re50_pysr.json new file mode 100644 index 0000000..513985a --- /dev/null +++ b/src/SR_analysis/results/archive/raw/karman_re50_pysr.json @@ -0,0 +1,7 @@ +{ + "scene": "karman_re50", + "mode": "pysr", + "similarity": 0.8468594520598547, + "action_range": 0.15152156686076074, + "n_steps": 200 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/karman_re50_v23.json b/src/SR_analysis/results/archive/raw/karman_re50_v23.json new file mode 100644 index 0000000..695fc63 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/karman_re50_v23.json @@ -0,0 +1,7 @@ +{ + "scene": "karman_re50", + "mode": "v23", + "similarity": 0.5824068239398507, + "action_range": 0.2252918496545665, + "n_steps": 200 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/karman_re70_deriv.json b/src/SR_analysis/results/archive/raw/karman_re70_deriv.json new file mode 100644 index 0000000..a7492a3 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/karman_re70_deriv.json @@ -0,0 +1,7 @@ +{ + "scene": "karman_re70", + "mode": "deriv", + "similarity": 0.4273366537100325, + "action_range": 0.25923786469074905, + "n_steps": 200 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/karman_re70_v23.json b/src/SR_analysis/results/archive/raw/karman_re70_v23.json new file mode 100644 index 0000000..a06fed6 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/karman_re70_v23.json @@ -0,0 +1,7 @@ +{ + "scene": "karman_re70", + "mode": "v23", + "similarity": 0.5769877353631374, + "action_range": 0.21521751562224534, + "n_steps": 200 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_illusion_0.75L_front.json b/src/SR_analysis/results/archive/raw/pysr_illusion_0.75L_front.json new file mode 100644 index 0000000..46bafb7 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_illusion_0.75L_front.json @@ -0,0 +1,94 @@ +{ + "scene": "illusion_0.75L", + "scene_id": "illusion", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "Cd_err", + "Cl_err", + "dCd_err_dt", + "dCl_err_dt" + ], + "equations": [ + { + "complexity": 1, + "loss": 1.1611668, + "equation": "Cd_err", + "score": 0.0, + "sympy_format": "Cd_err", + "lambda_format": "PySRFunction(X=>Cd_err)" + }, + { + "complexity": 2, + "loss": 0.075797155, + "equation": "-1.2395082", + "score": 2.729119881666683, + "sympy_format": "-1.23950820000000", + "lambda_format": "PySRFunction(X=>-1.23950820000000)" + }, + { + "complexity": 4, + "loss": 0.057897076, + "equation": "Cl_err + -1.2561985", + "score": 0.13469693826111778, + "sympy_format": "Cl_err - 1.2561985", + "lambda_format": "PySRFunction(X=>Cl_err - 1.2561985)" + }, + { + "complexity": 6, + "loss": 0.03754608, + "equation": "-1.2428339 - (dCl_tot_dt / Cd_tot)", + "score": 0.21654895191031456, + "sympy_format": "-1.2428339 - dCl_tot_dt/Cd_tot", + "lambda_format": "PySRFunction(X=>-1.2428339 - dCl_tot_dt/Cd_tot)" + }, + { + "complexity": 8, + "loss": 0.027650228, + "equation": "-1.2424065 - ((dCl_tot_dt + Cl_tot) / Cd_tot)", + "score": 0.15296750237696044, + "sympy_format": "-1.2424065 - (Cl_tot + dCl_tot_dt)/Cd_tot", + "lambda_format": "PySRFunction(X=>-1.2424065 - (Cl_tot + dCl_tot_dt)/Cd_tot)" + }, + { + "complexity": 9, + "loss": 0.023363287, + "equation": "-1.2398432 - ((Cl_tot + dCl_tot_dt) * 0.16933453)", + "score": 0.1684681151879758, + "sympy_format": "-0.16933453*(Cl_tot + dCl_tot_dt) - 1.2398432", + "lambda_format": "PySRFunction(X=>-0.16933453*(Cl_tot + dCl_tot_dt) - 1.2398432)" + }, + { + "complexity": 11, + "loss": 0.021574462, + "equation": "((dCl_err_dt - (Cl_tot + dCl_tot_dt)) * 0.20579989) + -1.2389864", + "score": 0.039827779143953225, + "sympy_format": "(dCl_err_dt - (Cl_tot + dCl_tot_dt))*0.20579989 - 1.2389864", + "lambda_format": "PySRFunction(X=>(dCl_err_dt - (Cl_tot + dCl_tot_dt))*0.20579989 - 1.2389864)" + }, + { + "complexity": 13, + "loss": 0.020914195, + "equation": "((Cl_err / Cd_tot) + -1.2402159) - ((Cl_tot + dCl_tot_dt) * 0.16267194)", + "score": 0.015541092679475423, + "sympy_format": "-0.16267194*(Cl_tot + dCl_tot_dt) - 1.2402159 + Cl_err/Cd_tot", + "lambda_format": "PySRFunction(X=>-0.16267194*(Cl_tot + dCl_tot_dt) - 1.2402159 + Cl_err/Cd_tot)" + }, + { + "complexity": 15, + "loss": 0.020151647, + "equation": "(((Cl_err / Cd_rear) / Cd_tot) + -1.2398432) - ((Cl_tot + dCl_tot_dt) * 0.16933453)", + "score": 0.01857104650284237, + "sympy_format": "-0.16933453*(Cl_tot + dCl_tot_dt) - 1.2398432 + Cl_err/(Cd_rear*Cd_tot)", + "lambda_format": "PySRFunction(X=>-0.16933453*(Cl_tot + dCl_tot_dt) - 1.2398432 + Cl_err/(Cd_rear*Cd_tot))" + } + ], + "best_sympy": "-0.16933453*(Cl_tot + dCl_tot_dt) - 1.2398432", + "best_score": 0.6917657052732269 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_illusion_0.75L_front_target.json b/src/SR_analysis/results/archive/raw/pysr_illusion_0.75L_front_target.json new file mode 100644 index 0000000..021ce74 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_illusion_0.75L_front_target.json @@ -0,0 +1,111 @@ +{ + "scene": "illusion_0.75L", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "Cd_err", + "Cl_err", + "dCd_err_dt", + "dCl_err_dt", + "target_Cd", + "target_Cl" + ], + "best_sympy": "-(dCl_tot_dt + target_Cl)/5.8401647 - 1.2372783", + "best_score": 0.7232722304555419, + "equations": [ + { + "complexity": 1, + "loss": 1.6359515, + "equation": "Cl_err", + "score": 0.0, + "sympy_format": "Cl_err", + "lambda_format": "PySRFunction(X=>Cl_err)" + }, + { + "complexity": 2, + "loss": 0.075797155, + "equation": "-1.2395153", + "score": 3.071919112284068, + "sympy_format": "-1.23951530000000", + "lambda_format": "PySRFunction(X=>-1.23951530000000)" + }, + { + "complexity": 4, + "loss": 0.057897076, + "equation": "Cl_err - 1.2562019", + "score": 0.13469693826111778, + "sympy_format": "Cl_err - 1*1.2562019", + "lambda_format": "PySRFunction(X=>Cl_err - 1*1.2562019)" + }, + { + "complexity": 6, + "loss": 0.037532225, + "equation": "-1.2431892 - (dCl_tot_dt / target_Cd)", + "score": 0.2167334925729904, + "sympy_format": "-dCl_tot_dt/target_Cd - 1.2431892", + "lambda_format": "PySRFunction(X=>-dCl_tot_dt/target_Cd - 1.2431892)" + }, + { + "complexity": 8, + "loss": 0.026256593, + "equation": "-1.2326334 - ((Cl_tot + dCl_tot_dt) / target_Cd)", + "score": 0.1786413889438378, + "sympy_format": "-1.2326334 - (Cl_tot + dCl_tot_dt)/target_Cd", + "lambda_format": "PySRFunction(X=>-1.2326334 - (Cl_tot + dCl_tot_dt)/target_Cd)" + }, + { + "complexity": 9, + "loss": 0.020975176, + "equation": "-1.2372783 - ((target_Cl + dCl_tot_dt) / 5.8401647)", + "score": 0.22457747614686055, + "sympy_format": "-(dCl_tot_dt + target_Cl)/5.8401647 - 1.2372783", + "lambda_format": "PySRFunction(X=>-(dCl_tot_dt + target_Cl)/5.8401647 - 1.2372783)" + }, + { + "complexity": 10, + "loss": 0.019975036, + "equation": "-1.2351149 - ((dCl_tot_dt + target_Cl) / (target_Cd + Cd_rear))", + "score": 0.0488563493561329, + "sympy_format": "-1.2351149 - (dCl_tot_dt + target_Cl)/(Cd_rear + target_Cd)", + "lambda_format": "PySRFunction(X=>-1.2351149 - (dCl_tot_dt + target_Cl)/(Cd_rear + target_Cd))" + }, + { + "complexity": 11, + "loss": 0.018287793, + "equation": "-1.2386359 - ((target_Cl + (dCl_tot_dt + dCd_err_dt)) / 5.8050957)", + "score": 0.08824950581277256, + "sympy_format": "-(dCd_err_dt + dCl_tot_dt + target_Cl)/5.8050957 - 1.2386359", + "lambda_format": "PySRFunction(X=>-(dCd_err_dt + dCl_tot_dt + target_Cl)/5.8050957 - 1.2386359)" + }, + { + "complexity": 12, + "loss": 0.016175408, + "equation": "-1.2290057 - ((((dCd_err_dt + target_Cl) + dCl_tot_dt) / target_Cd) / Cd_rear)", + "score": 0.12274172391063341, + "sympy_format": "-1.2290057 - (dCd_err_dt + dCl_tot_dt + target_Cl)/(Cd_rear*target_Cd)", + "lambda_format": "PySRFunction(X=>-1.2290057 - (dCd_err_dt + dCl_tot_dt + target_Cl)/(Cd_rear*target_Cd))" + }, + { + "complexity": 13, + "loss": 0.01565276, + "equation": "-1.2277503 - ((((dCl_tot_dt + dCd_err_dt) + target_Cl) / 3.854052) / Cd_rear)", + "score": 0.03284480491569963, + "sympy_format": "-1.2277503 - (dCd_err_dt + dCl_tot_dt + target_Cl)/(3.854052*Cd_rear)", + "lambda_format": "PySRFunction(X=>-1.2277503 - (dCd_err_dt + dCl_tot_dt + target_Cl)/(3.854052*Cd_rear))" + }, + { + "complexity": 14, + "loss": 0.014618657, + "equation": "-1.1933857 - (((dCl_tot_dt + dCd_err_dt) + target_Cl) / ((target_Cd - target_Cl) * Cd_rear))", + "score": 0.0683486696273396, + "sympy_format": "-1.1933857 - (dCd_err_dt + dCl_tot_dt + target_Cl)/(Cd_rear*(target_Cd - target_Cl))", + "lambda_format": "PySRFunction(X=>-1.1933857 - (dCd_err_dt + dCl_tot_dt + target_Cl)/(Cd_rear*(target_Cd - target_Cl)))" + } + ] +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_illusion_0.75L_top.json b/src/SR_analysis/results/archive/raw/pysr_illusion_0.75L_top.json new file mode 100644 index 0000000..d2a4fb9 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_illusion_0.75L_top.json @@ -0,0 +1,119 @@ +{ + "scene": "illusion_0.75L", + "scene_id": "illusion", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "Cd_err", + "Cl_err", + "dCd_err_dt", + "dCl_err_dt" + ], + "equations": [ + { + "complexity": 1, + "loss": 0.45252356, + "equation": "Cd_rear", + "score": 0.0, + "sympy_format": "Cd_rear", + "lambda_format": "PySRFunction(X=>Cd_rear)" + }, + { + "complexity": 2, + "loss": 0.1519909, + "equation": "1.8688796", + "score": 1.0910191774498823, + "sympy_format": "1.86887960000000", + "lambda_format": "PySRFunction(X=>1.86887960000000)" + }, + { + "complexity": 4, + "loss": 0.1296351, + "equation": "Cl_err + 1.8521569", + "score": 0.07954853502512871, + "sympy_format": "Cl_err + 1.8521569", + "lambda_format": "PySRFunction(X=>Cl_err + 1.8521569)" + }, + { + "complexity": 6, + "loss": 0.075855345, + "equation": "(dCl_tot_dt - Cd_tot) * -0.45136496", + "score": 0.267947704620995, + "sympy_format": "(-Cd_tot + dCl_tot_dt)*(-0.45136496)", + "lambda_format": "PySRFunction(X=>(-Cd_tot + dCl_tot_dt)*(-0.45136496))" + }, + { + "complexity": 7, + "loss": 0.06326852, + "equation": "(u_a * -0.016224489) + 1.88584", + "score": 0.18144028026845632, + "sympy_format": "1.88584 + u_a*(-0.016224489)", + "lambda_format": "PySRFunction(X=>1.88584 + u_a*(-0.016224489))" + }, + { + "complexity": 8, + "loss": 0.056762762, + "equation": "((dCl_tot_dt - Cd_tot) - dCl_err_dt) * -0.44981483", + "score": 0.10850737893550855, + "sympy_format": "(-Cd_tot - dCl_err_dt + dCl_tot_dt)*(-0.44981483)", + "lambda_format": "PySRFunction(X=>(-Cd_tot - dCl_err_dt + dCl_tot_dt)*(-0.44981483))" + }, + { + "complexity": 9, + "loss": 0.039528407, + "equation": "(Cl_err - (u_a * 0.016350273)) + 1.8692697", + "score": 0.36186093414770126, + "sympy_format": "Cl_err - 0.016350273*u_a + 1.8692697", + "lambda_format": "PySRFunction(X=>Cl_err - 0.016350273*u_a + 1.8692697)" + }, + { + "complexity": 10, + "loss": 0.038722, + "equation": "Cl_err + square((u_a * 0.00608778) - 1.3626369)", + "score": 0.020611664022151262, + "sympy_format": "Cl_err + (u_a*0.00608778 - 1*1.3626369)**2", + "lambda_format": "PySRFunction(X=>Cl_err + (u_a*0.00608778 - 1*1.3626369)**2)" + }, + { + "complexity": 11, + "loss": 0.03458842, + "equation": "Cd_err + (Cl_err + ((u_a * -0.014238624) - -2.0921295))", + "score": 0.11288897000276664, + "sympy_format": "Cd_err + Cl_err + u_a*(-0.014238624) - 1*(-2.0921295)", + "lambda_format": "PySRFunction(X=>Cd_err + Cl_err + u_a*(-0.014238624) - 1*(-2.0921295))" + }, + { + "complexity": 12, + "loss": 0.031432744, + "equation": "(((u_a * -0.015429008) - -1.9807403) - square(Cd_err)) + Cl_err", + "score": 0.09566879184310657, + "sympy_format": "-Cd_err**2 + Cl_err + u_a*(-0.015429008) - 1*(-1.9807403)", + "lambda_format": "PySRFunction(X=>-Cd_err**2 + Cl_err + u_a*(-0.015429008) - 1*(-1.9807403))" + }, + { + "complexity": 13, + "loss": 0.030870467, + "equation": "(Cl_err + square((u_a * 0.0055642533) - 1.4036555)) - square(Cd_err)", + "score": 0.01805018576551437, + "sympy_format": "-Cd_err**2 + Cl_err + (u_a*0.0055642533 - 1*1.4036555)**2", + "lambda_format": "PySRFunction(X=>-Cd_err**2 + Cl_err + (u_a*0.0055642533 - 1*1.4036555)**2)" + }, + { + "complexity": 14, + "loss": 0.021888444, + "equation": "((Cl_err + (Cd_err * 0.5409948)) + (u_a * -0.015207872)) + 1.9898309", + "score": 0.3438411400334662, + "sympy_format": "Cd_err*0.5409948 + Cl_err + u_a*(-0.015207872) + 1.9898309", + "lambda_format": "PySRFunction(X=>Cd_err*0.5409948 + Cl_err + u_a*(-0.015207872) + 1.9898309)" + } + ], + "best_sympy": "Cd_err*0.5409948 + Cl_err + u_a*(-0.015207872) + 1.9898309", + "best_score": 0.8559884564869915 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_illusion_0.75L_top_target.json b/src/SR_analysis/results/archive/raw/pysr_illusion_0.75L_top_target.json new file mode 100644 index 0000000..3125a3d --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_illusion_0.75L_top_target.json @@ -0,0 +1,112 @@ +{ + "scene": "illusion_0.75L", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "Cd_err", + "Cl_err", + "dCd_err_dt", + "dCl_err_dt", + "target_Cd", + "target_Cl" + ], + "best_sympy": "Cd_tot*0.45037818 + Cl_err - 0.016348997*u_a", + "best_score": 0.8375927169907114, + "equations": [ + { + "complexity": 1, + "loss": 0.45252356, + "equation": "Cd_rear", + "score": 0.0, + "sympy_format": "Cd_rear", + "lambda_format": "PySRFunction(X=>Cd_rear)" + }, + { + "complexity": 2, + "loss": 0.1519909, + "equation": "1.8688794", + "score": 1.0910191774498823, + "sympy_format": "1.86887940000000", + "lambda_format": "PySRFunction(X=>1.86887940000000)" + }, + { + "complexity": 4, + "loss": 0.1296351, + "equation": "Cl_err + 1.8521743", + "score": 0.07954853502512871, + "sympy_format": "Cl_err + 1.8521743", + "lambda_format": "PySRFunction(X=>Cl_err + 1.8521743)" + }, + { + "complexity": 6, + "loss": 0.074802674, + "equation": "(8.007482 - dCl_tot_dt) / target_Cd", + "score": 0.2749349737891081, + "sympy_format": "(8.007482 - dCl_tot_dt)/target_Cd", + "lambda_format": "PySRFunction(X=>(8.007482 - dCl_tot_dt)/target_Cd)" + }, + { + "complexity": 7, + "loss": 0.06326852, + "equation": "1.8858196 - (u_a * 0.01622415)", + "score": 0.16746574193223032, + "sympy_format": "1.8858196 - 0.01622415*u_a", + "lambda_format": "PySRFunction(X=>1.8858196 - 0.01622415*u_a)" + }, + { + "complexity": 8, + "loss": 0.04728117, + "equation": "((Cl_tot + 7.9624605) - dCl_tot_dt) / target_Cd", + "score": 0.29127577208778055, + "sympy_format": "(Cl_tot - dCl_tot_dt + 7.9624605)/target_Cd", + "lambda_format": "PySRFunction(X=>(Cl_tot - dCl_tot_dt + 7.9624605)/target_Cd)" + }, + { + "complexity": 9, + "loss": 0.039528407, + "equation": "(Cl_err + 1.8692691) - (u_a * 0.016348997)", + "score": 0.17909254099542934, + "sympy_format": "Cl_err - 0.016348997*u_a + 1.8692691", + "lambda_format": "PySRFunction(X=>Cl_err - 0.016348997*u_a + 1.8692691)" + }, + { + "complexity": 10, + "loss": 0.037286226, + "equation": "(((target_Cd - dCl_tot_dt) * 1.8496238) + target_Cl) / target_Cd", + "score": 0.05839559567798887, + "sympy_format": "(target_Cl + (-dCl_tot_dt + target_Cd)*1.8496238)/target_Cd", + "lambda_format": "PySRFunction(X=>(target_Cl + (-dCl_tot_dt + target_Cd)*1.8496238)/target_Cd)" + }, + { + "complexity": 11, + "loss": 0.024684431, + "equation": "Cl_err + ((Cd_tot * 0.45037818) - (u_a * 0.016348997))", + "score": 0.41245126130146226, + "sympy_format": "Cd_tot*0.45037818 + Cl_err - 0.016348997*u_a", + "lambda_format": "PySRFunction(X=>Cd_tot*0.45037818 + Cl_err - 0.016348997*u_a)" + }, + { + "complexity": 13, + "loss": 0.018228356, + "equation": "(((dCl_err_dt + target_Cd) - dCl_tot_dt) * 0.4332428) + (du_a_dt / -94.089226)", + "score": 0.15159715870297544, + "sympy_format": "du_a_dt/(-94.089226) + (dCl_err_dt - dCl_tot_dt + target_Cd)*0.4332428", + "lambda_format": "PySRFunction(X=>du_a_dt/(-94.089226) + (dCl_err_dt - dCl_tot_dt + target_Cd)*0.4332428)" + }, + { + "complexity": 15, + "loss": 0.017791519, + "equation": "(((dCl_err_dt + target_Cd) - dCl_tot_dt) * 0.432934) + ((du_a_dt + dCl_tot_dt) / -91.89213)", + "score": 0.012128260261647603, + "sympy_format": "(dCl_tot_dt + du_a_dt)/(-91.89213) + (dCl_err_dt - dCl_tot_dt + target_Cd)*0.432934", + "lambda_format": "PySRFunction(X=>(dCl_tot_dt + du_a_dt)/(-91.89213) + (dCl_err_dt - dCl_tot_dt + target_Cd)*0.432934)" + } + ] +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_illusion_1.5L_front.json b/src/SR_analysis/results/archive/raw/pysr_illusion_1.5L_front.json new file mode 100644 index 0000000..13c98e1 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_illusion_1.5L_front.json @@ -0,0 +1,45 @@ +{ + "scene": "illusion_1.5L", + "channel": "front", + "feature_names": [ + "Cd_tot", + "Cd_rear", + "Cl_tot", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT", + "target_Cd", + "target_Cl" + ], + "equations": [ + { + "complexity": 1, + "loss": 9.924878, + "equation": "Cl_tot", + "score": 0.0, + "sympy_format": "Cl_tot", + "lambda_format": "PySRFunction(X=>Cl_tot)" + }, + { + "complexity": 2, + "loss": 0.0019758705, + "equation": "0.0019602603", + "score": 8.521790752547583, + "sympy_format": "0.00196026030000000", + "lambda_format": "PySRFunction(X=>0.00196026030000000)" + }, + { + "complexity": 4, + "loss": 2.7072198999999996e-18, + "equation": "aF_lag1 * 0.01", + "score": 17.11193160724022, + "sympy_format": "aF_lag1*0.01", + "lambda_format": "PySRFunction(X=>aF_lag1*0.01)" + } + ], + "best_sympy": "aF_lag1*0.01", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_illusion_1.5L_top.json b/src/SR_analysis/results/archive/raw/pysr_illusion_1.5L_top.json new file mode 100644 index 0000000..4b27fcd --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_illusion_1.5L_top.json @@ -0,0 +1,55 @@ +{ + "scene": "illusion_1.5L", + "channel": "top", + "feature_names": [ + "bias", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT", + "target_Cd", + "target_Cl" + ], + "equations": [ + { + "complexity": 1, + "loss": 1.0037948, + "equation": "bias", + "score": 0.0, + "sympy_format": "bias", + "lambda_format": "PySRFunction(X=>bias)" + }, + { + "complexity": 2, + "loss": 0.0007045742, + "equation": "-0.0015421732", + "score": 7.261704527158024, + "sympy_format": "-0.00154217320000000", + "lambda_format": "PySRFunction(X=>-0.00154217320000000)" + }, + { + "complexity": 4, + "loss": 2.5116741e-13, + "equation": "aT_lag1 / 99.998116", + "score": 10.877369898373423, + "sympy_format": "aT_lag1/99.998116", + "lambda_format": "PySRFunction(X=>aT_lag1/99.998116)" + }, + { + "complexity": 6, + "loss": 1.5754642e-18, + "equation": "(aT_lag1 + aT_lag1) * 0.005", + "score": 5.989662504443251, + "sympy_format": "(aT_lag1 + aT_lag1)*0.005", + "lambda_format": "PySRFunction(X=>(aT_lag1 + aT_lag1)*0.005)" + } + ], + "best_sympy": "(aT_lag1 + aT_lag1)*0.005", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_illusion_1L_front.json b/src/SR_analysis/results/archive/raw/pysr_illusion_1L_front.json new file mode 100644 index 0000000..811f7d4 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_illusion_1L_front.json @@ -0,0 +1,110 @@ +{ + "scene": "illusion_1L", + "scene_id": "illusion", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "Cd_err", + "Cl_err", + "dCd_err_dt", + "dCl_err_dt" + ], + "equations": [ + { + "complexity": 1, + "loss": 0.48229608, + "equation": "dCl_err_dt", + "score": 0.0, + "sympy_format": "dCl_err_dt", + "lambda_format": "PySRFunction(X=>dCl_err_dt)" + }, + { + "complexity": 2, + "loss": 0.15649326, + "equation": "0.33449104", + "score": 1.1255452573604185, + "sympy_format": "0.334491040000000", + "lambda_format": "PySRFunction(X=>0.334491040000000)" + }, + { + "complexity": 4, + "loss": 0.14671756, + "equation": "1.1508505 / Cd_rear", + "score": 0.03225178195326072, + "sympy_format": "1.1508505/Cd_rear", + "lambda_format": "PySRFunction(X=>1.1508505/Cd_rear)" + }, + { + "complexity": 5, + "loss": 0.14011887, + "equation": "(Cd_tot - Cd_rear) / Cd_tot", + "score": 0.0460182442729821, + "sympy_format": "(-Cd_rear + Cd_tot)/Cd_tot", + "lambda_format": "PySRFunction(X=>(-Cd_rear + Cd_tot)/Cd_tot)" + }, + { + "complexity": 6, + "loss": 0.0863823, + "equation": "(Cl_tot + 1.8902351) / Cd_tot", + "score": 0.4837083400686436, + "sympy_format": "(Cl_tot + 1.8902351)/Cd_tot", + "lambda_format": "PySRFunction(X=>(Cl_tot + 1.8902351)/Cd_tot)" + }, + { + "complexity": 7, + "loss": 0.047962923, + "equation": "(u_a * 0.015167036) + 0.31766427", + "score": 0.5883545187876664, + "sympy_format": "u_a*0.015167036 + 0.31766427", + "lambda_format": "PySRFunction(X=>u_a*0.015167036 + 0.31766427)" + }, + { + "complexity": 8, + "loss": 0.043905187, + "equation": "((Cl_tot - dCl_tot_dt) + 1.8545729) / Cd_tot", + "score": 0.08839580692544886, + "sympy_format": "(Cl_tot - dCl_tot_dt + 1.8545729)/Cd_tot", + "lambda_format": "PySRFunction(X=>(Cl_tot - dCl_tot_dt + 1.8545729)/Cd_tot)" + }, + { + "complexity": 9, + "loss": 0.011135678, + "equation": "((u_a + du_a_dt) + 26.506319) * 0.012334255", + "score": 1.3718782800547544, + "sympy_format": "(du_a_dt + u_a + 26.506319)*0.012334255", + "lambda_format": "PySRFunction(X=>(du_a_dt + u_a + 26.506319)*0.012334255)" + }, + { + "complexity": 11, + "loss": 0.010412063, + "equation": "(u_a + ((26.81757 - dCl_tot_dt) + du_a_dt)) * 0.01217924", + "score": 0.03359457505969681, + "sympy_format": "(-dCl_tot_dt + du_a_dt + u_a + 26.81757)*0.01217924", + "lambda_format": "PySRFunction(X=>(-dCl_tot_dt + du_a_dt + u_a + 26.81757)*0.01217924)" + }, + { + "complexity": 12, + "loss": 0.009215188, + "equation": "(du_a_dt * 0.010480981) + ((u_a * 0.0138015505) + 0.32438698)", + "score": 0.12211204539179134, + "sympy_format": "du_a_dt*0.010480981 + u_a*0.0138015505 + 0.32438698", + "lambda_format": "PySRFunction(X=>du_a_dt*0.010480981 + u_a*0.0138015505 + 0.32438698)" + }, + { + "complexity": 14, + "loss": 0.008032124, + "equation": "((du_a_dt * 0.009704931) + ((u_a + Cl_tot) * 0.013764588)) + 0.32533023", + "score": 0.06870199567786181, + "sympy_format": "du_a_dt*0.009704931 + (Cl_tot + u_a)*0.013764588 + 0.32533023", + "lambda_format": "PySRFunction(X=>du_a_dt*0.009704931 + (Cl_tot + u_a)*0.013764588 + 0.32533023)" + } + ], + "best_sympy": "(du_a_dt + u_a + 26.506319)*0.012334255", + "best_score": 0.9288424393884038 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_illusion_1L_front_target.json b/src/SR_analysis/results/archive/raw/pysr_illusion_1L_front_target.json new file mode 100644 index 0000000..93f94d8 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_illusion_1L_front_target.json @@ -0,0 +1,127 @@ +{ + "scene": "illusion_1L", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "Cd_err", + "Cl_err", + "dCd_err_dt", + "dCl_err_dt", + "target_Cd", + "target_Cl" + ], + "best_sympy": "(du_a_dt + u_a + 26.506351)*0.012334249", + "best_score": 0.9288424393879982, + "equations": [ + { + "complexity": 1, + "loss": 0.29883155, + "equation": "dCd_err_dt", + "score": 0.0, + "sympy_format": "dCd_err_dt", + "lambda_format": "PySRFunction(X=>dCd_err_dt)" + }, + { + "complexity": 2, + "loss": 0.15649326, + "equation": "0.33446583", + "score": 0.6468670947471492, + "sympy_format": "0.334465830000000", + "lambda_format": "PySRFunction(X=>0.334465830000000)" + }, + { + "complexity": 4, + "loss": 0.14671756, + "equation": "1.1508491 / Cd_rear", + "score": 0.03225178195326072, + "sympy_format": "1.1508491/Cd_rear", + "lambda_format": "PySRFunction(X=>1.1508491/Cd_rear)" + }, + { + "complexity": 5, + "loss": 0.14011887, + "equation": "(Cd_tot - Cd_rear) / Cd_tot", + "score": 0.0460182442729821, + "sympy_format": "(-Cd_rear + Cd_tot)/Cd_tot", + "lambda_format": "PySRFunction(X=>(-Cd_rear + Cd_tot)/Cd_tot)" + }, + { + "complexity": 6, + "loss": 0.08638231, + "equation": "(Cl_tot + 1.8901986) / Cd_tot", + "score": 0.48370822430419386, + "sympy_format": "(Cl_tot + 1.8901986)/Cd_tot", + "lambda_format": "PySRFunction(X=>(Cl_tot + 1.8901986)/Cd_tot)" + }, + { + "complexity": 7, + "loss": 0.047962923, + "equation": "(u_a * 0.015167245) + 0.3176641", + "score": 0.5883546345521161, + "sympy_format": "u_a*0.015167245 + 0.3176641", + "lambda_format": "PySRFunction(X=>u_a*0.015167245 + 0.3176641)" + }, + { + "complexity": 8, + "loss": 0.04292227, + "equation": "((Cl_tot - dCl_tot_dt) / target_Cd) + 0.34500188", + "score": 0.11103746946441734, + "sympy_format": "0.34500188 + (Cl_tot - dCl_tot_dt)/target_Cd", + "lambda_format": "PySRFunction(X=>0.34500188 + (Cl_tot - dCl_tot_dt)/target_Cd)" + }, + { + "complexity": 9, + "loss": 0.011135677, + "equation": "(u_a + (du_a_dt + 26.506351)) * 0.012334249", + "score": 1.349236707317237, + "sympy_format": "(du_a_dt + u_a + 26.506351)*0.012334249", + "lambda_format": "PySRFunction(X=>(du_a_dt + u_a + 26.506351)*0.012334249)" + }, + { + "complexity": 11, + "loss": 0.010412063, + "equation": "((du_a_dt - dCl_tot_dt) + (u_a + 26.817722)) * 0.012179124", + "score": 0.03359453015897125, + "sympy_format": "(-dCl_tot_dt + du_a_dt + u_a + 26.817722)*0.012179124", + "lambda_format": "PySRFunction(X=>(-dCl_tot_dt + du_a_dt + u_a + 26.817722)*0.012179124)" + }, + { + "complexity": 12, + "loss": 0.009215188, + "equation": "(du_a_dt * 0.010480981) + ((u_a * 0.0138015505) + 0.32438707)", + "score": 0.12211204539179134, + "sympy_format": "du_a_dt*0.010480981 + u_a*0.0138015505 + 0.32438707", + "lambda_format": "PySRFunction(X=>du_a_dt*0.010480981 + u_a*0.0138015505 + 0.32438707)" + }, + { + "complexity": 13, + "loss": 0.00894252, + "equation": "(((du_a_dt + u_a) + 22.995256) - (du_a_dt / Cd_rear)) * 0.014093986", + "score": 0.030035563773362823, + "sympy_format": "(du_a_dt + u_a + 22.995256 - du_a_dt/Cd_rear)*0.014093986", + "lambda_format": "PySRFunction(X=>(du_a_dt + u_a + 22.995256 - du_a_dt/Cd_rear)*0.014093986)" + }, + { + "complexity": 14, + "loss": 0.008032124, + "equation": "((u_a + Cl_tot) + ((du_a_dt / 1.4181519) + 23.637499)) * 0.013763895", + "score": 0.10736842758236086, + "sympy_format": "(Cl_tot + du_a_dt/1.4181519 + u_a + 23.637499)*0.013763895", + "lambda_format": "PySRFunction(X=>(Cl_tot + du_a_dt/1.4181519 + u_a + 23.637499)*0.013763895)" + }, + { + "complexity": 15, + "loss": 0.0077406703, + "equation": "(du_a_dt + (((Cl_tot + 23.718166) - (du_a_dt / Cd_rear)) + u_a)) * 0.013728722", + "score": 0.0369607151581401, + "sympy_format": "(Cl_tot + du_a_dt + u_a + 23.718166 - du_a_dt/Cd_rear)*0.013728722", + "lambda_format": "PySRFunction(X=>(Cl_tot + du_a_dt + u_a + 23.718166 - du_a_dt/Cd_rear)*0.013728722)" + } + ] +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_illusion_1L_top.json b/src/SR_analysis/results/archive/raw/pysr_illusion_1L_top.json new file mode 100644 index 0000000..505177d --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_illusion_1L_top.json @@ -0,0 +1,135 @@ +{ + "scene": "illusion_1L", + "scene_id": "illusion", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "Cd_err", + "Cl_err", + "dCd_err_dt", + "dCl_err_dt" + ], + "equations": [ + { + "complexity": 1, + "loss": 0.11277538, + "equation": "bias", + "score": 0.0, + "sympy_format": "bias", + "lambda_format": "PySRFunction(X=>bias)" + }, + { + "complexity": 2, + "loss": 0.06636527, + "equation": "0.7845695", + "score": 0.5302241753504614, + "sympy_format": "0.784569500000000", + "lambda_format": "PySRFunction(X=>0.784569500000000)" + }, + { + "complexity": 3, + "loss": 0.066365264, + "equation": "square(0.88574606)", + "score": 9.040873758650598e-08, + "sympy_format": "0.784546082805524", + "lambda_format": "PySRFunction(X=>0.784546082805524)" + }, + { + "complexity": 4, + "loss": 0.065482475, + "equation": "2.5905075 / Cd_rear", + "score": 0.013391237481234886, + "sympy_format": "2.5905075/Cd_rear", + "lambda_format": "PySRFunction(X=>2.5905075/Cd_rear)" + }, + { + "complexity": 5, + "loss": 0.05560235, + "equation": "Cd_tot / (Cd_rear + Cd_rear)", + "score": 0.16355708303907351, + "sympy_format": "Cd_tot/(Cd_rear + Cd_rear)", + "lambda_format": "PySRFunction(X=>Cd_tot/(Cd_rear + Cd_rear))" + }, + { + "complexity": 6, + "loss": 0.037583124, + "equation": "0.7779934 - (dCl_tot_dt / Cd_tot)", + "score": 0.3916703466960199, + "sympy_format": "0.7779934 - dCl_tot_dt/Cd_tot", + "lambda_format": "PySRFunction(X=>0.7779934 - dCl_tot_dt/Cd_tot)" + }, + { + "complexity": 7, + "loss": 0.035889544, + "equation": "(dCl_tot_dt * -0.14946744) + 0.7795025", + "score": 0.04610912026457068, + "sympy_format": "0.7795025 + dCl_tot_dt*(-0.14946744)", + "lambda_format": "PySRFunction(X=>0.7795025 + dCl_tot_dt*(-0.14946744))" + }, + { + "complexity": 8, + "loss": 0.029347742, + "equation": "0.7866203 - (dCl_tot_dt / (Cd_tot - Cl_tot))", + "score": 0.2012303898357161, + "sympy_format": "0.7866203 - dCl_tot_dt/(Cd_tot - Cl_tot)", + "lambda_format": "PySRFunction(X=>0.7866203 - dCl_tot_dt/(Cd_tot - Cl_tot))" + }, + { + "complexity": 10, + "loss": 0.02741706, + "equation": "(dCl_tot_dt / (Cl_tot + (Cl_err - Cd_tot))) + 0.7815183", + "score": 0.03402508118753478, + "sympy_format": "dCl_tot_dt/(-Cd_tot + Cl_err + Cl_tot) + 0.7815183", + "lambda_format": "PySRFunction(X=>dCl_tot_dt/(-Cd_tot + Cl_err + Cl_tot) + 0.7815183)" + }, + { + "complexity": 11, + "loss": 0.025678542, + "equation": "0.77942413 - (((dCl_tot_dt * Cl_tot) + dCl_tot_dt) / square(Cd_rear))", + "score": 0.06550974586999601, + "sympy_format": "0.77942413 - (Cl_tot*dCl_tot_dt + dCl_tot_dt)/(Cd_rear**2)", + "lambda_format": "PySRFunction(X=>0.77942413 - (Cl_tot*dCl_tot_dt + dCl_tot_dt)/(Cd_rear**2))" + }, + { + "complexity": 12, + "loss": 0.020175973, + "equation": "0.7962736 - ((Cl_tot + bias) * (dCl_tot_dt / (Cd_rear + Cd_rear)))", + "score": 0.2411632605109884, + "sympy_format": "0.7962736 - dCl_tot_dt*(Cl_tot + bias)/(Cd_rear + Cd_rear)", + "lambda_format": "PySRFunction(X=>0.7962736 - dCl_tot_dt*(Cl_tot + bias)/(Cd_rear + Cd_rear))" + }, + { + "complexity": 13, + "loss": 0.018688664, + "equation": "0.8023191 - ((dCl_tot_dt * (Cl_tot + 1.3881962)) / (Cd_rear + Cd_tot))", + "score": 0.07657530416150655, + "sympy_format": "0.8023191 - dCl_tot_dt*(Cl_tot + 1.3881962)/(Cd_rear + Cd_tot)", + "lambda_format": "PySRFunction(X=>0.8023191 - dCl_tot_dt*(Cl_tot + 1.3881962)/(Cd_rear + Cd_tot))" + }, + { + "complexity": 14, + "loss": 0.016381161, + "equation": "0.7874223 - (dCl_tot_dt * (((Cl_tot + bias) + Cl_err) / (Cd_rear + Cd_rear)))", + "score": 0.13178518180253324, + "sympy_format": "0.7874223 - dCl_tot_dt*(Cl_err + Cl_tot + bias)/(Cd_rear + Cd_rear)", + "lambda_format": "PySRFunction(X=>0.7874223 - dCl_tot_dt*(Cl_err + Cl_tot + bias)/(Cd_rear + Cd_rear))" + }, + { + "complexity": 15, + "loss": 0.015024375, + "equation": "0.78738326 - ((dCl_tot_dt * ((Cl_err + 1.4685087) + Cl_tot)) / (Cd_rear + Cd_tot))", + "score": 0.08645807281568882, + "sympy_format": "0.78738326 - dCl_tot_dt*(Cl_err + Cl_tot + 1.4685087)/(Cd_rear + Cd_tot)", + "lambda_format": "PySRFunction(X=>0.78738326 - dCl_tot_dt*(Cl_err + Cl_tot + 1.4685087)/(Cd_rear + Cd_tot))" + } + ], + "best_sympy": "0.7962736 - dCl_tot_dt*(Cl_tot + bias)/(Cd_rear + Cd_rear)", + "best_score": 0.6959860204249317 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_illusion_1L_top_target.json b/src/SR_analysis/results/archive/raw/pysr_illusion_1L_top_target.json new file mode 100644 index 0000000..36e7bd8 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_illusion_1L_top_target.json @@ -0,0 +1,136 @@ +{ + "scene": "illusion_1L", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "Cd_err", + "Cl_err", + "dCd_err_dt", + "dCl_err_dt", + "target_Cd", + "target_Cl" + ], + "best_sympy": "dCl_tot_dt*(-0.13315448)*(Cl_tot + bias) + 0.80922115", + "best_score": 0.6947513140376443, + "equations": [ + { + "complexity": 1, + "loss": 0.11277538, + "equation": "bias", + "score": 0.0, + "sympy_format": "bias", + "lambda_format": "PySRFunction(X=>bias)" + }, + { + "complexity": 2, + "loss": 0.066365264, + "equation": "0.7845461", + "score": 0.530224265759199, + "sympy_format": "0.784546100000000", + "lambda_format": "PySRFunction(X=>0.784546100000000)" + }, + { + "complexity": 4, + "loss": 0.0651199, + "equation": "target_Cd + -4.860893", + "score": 0.00947180050206764, + "sympy_format": "target_Cd - 4.860893", + "lambda_format": "PySRFunction(X=>target_Cd - 4.860893)" + }, + { + "complexity": 5, + "loss": 0.05987383, + "equation": "Cd_rear / (Cd_tot + dCl_tot_dt)", + "score": 0.08399067123622497, + "sympy_format": "Cd_rear/(Cd_tot + dCl_tot_dt)", + "lambda_format": "PySRFunction(X=>Cd_rear/(Cd_tot + dCl_tot_dt))" + }, + { + "complexity": 6, + "loss": 0.03714929, + "equation": "0.77852744 - (dCl_tot_dt / target_Cd)", + "score": 0.47729485555849455, + "sympy_format": "0.77852744 - dCl_tot_dt/target_Cd", + "lambda_format": "PySRFunction(X=>0.77852744 - dCl_tot_dt/target_Cd)" + }, + { + "complexity": 7, + "loss": 0.03588954, + "equation": "(dCl_tot_dt * -0.14946803) + 0.77950186", + "score": 0.034498771135124374, + "sympy_format": "0.77950186 + dCl_tot_dt*(-0.14946803)", + "lambda_format": "PySRFunction(X=>0.77950186 + dCl_tot_dt*(-0.14946803))" + }, + { + "complexity": 8, + "loss": 0.028979579, + "equation": "(dCl_tot_dt / (Cl_tot - target_Cd)) + 0.78621364", + "score": 0.21385447860989099, + "sympy_format": "dCl_tot_dt/(Cl_tot - target_Cd) + 0.78621364", + "lambda_format": "PySRFunction(X=>dCl_tot_dt/(Cl_tot - target_Cd) + 0.78621364)" + }, + { + "complexity": 9, + "loss": 0.028761907, + "equation": "(dCl_tot_dt / (Cl_tot + -5.7742095)) + 0.7856848", + "score": 0.007539571500876667, + "sympy_format": "dCl_tot_dt/(Cl_tot - 5.7742095) + 0.7856848", + "lambda_format": "PySRFunction(X=>dCl_tot_dt/(Cl_tot - 5.7742095) + 0.7856848)" + }, + { + "complexity": 10, + "loss": 0.026970413, + "equation": "(dCl_tot_dt / ((Cl_tot + Cl_tot) - target_Cd)) + 0.80081576", + "score": 0.06431138768048111, + "sympy_format": "dCl_tot_dt/(Cl_tot + Cl_tot - target_Cd) + 0.80081576", + "lambda_format": "PySRFunction(X=>dCl_tot_dt/(Cl_tot + Cl_tot - target_Cd) + 0.80081576)" + }, + { + "complexity": 11, + "loss": 0.02025791, + "equation": "((dCl_tot_dt * -0.13315448) * (Cl_tot + bias)) + 0.80922115", + "score": 0.2861951157800486, + "sympy_format": "dCl_tot_dt*(-0.13315448)*(Cl_tot + bias) + 0.80922115", + "lambda_format": "PySRFunction(X=>dCl_tot_dt*(-0.13315448)*(Cl_tot + bias) + 0.80922115)" + }, + { + "complexity": 12, + "loss": 0.0187998, + "equation": "(((Cl_tot + 1.4032292) * dCl_tot_dt) * -0.11100898) + 0.80360377", + "score": 0.0746991030839848, + "sympy_format": "(Cl_tot + 1.4032292)*dCl_tot_dt*(-0.11100898) + 0.80360377", + "lambda_format": "PySRFunction(X=>(Cl_tot + 1.4032292)*dCl_tot_dt*(-0.11100898) + 0.80360377)" + }, + { + "complexity": 13, + "loss": 0.017390246, + "equation": "square(((dCl_tot_dt * (Cl_tot + 1.4040092)) * -0.065458074) + 0.88719195)", + "score": 0.0779367571543523, + "sympy_format": "(dCl_tot_dt*(Cl_tot + 1.4040092)*(-0.065458074) + 0.88719195)**2", + "lambda_format": "PySRFunction(X=>(dCl_tot_dt*(Cl_tot + 1.4040092)*(-0.065458074) + 0.88719195)**2)" + }, + { + "complexity": 14, + "loss": 0.015813593, + "equation": "((dCl_tot_dt * -0.11452771) * ((Cl_err + Cl_tot) + 1.4403551)) + 0.78953165", + "score": 0.09503958769856034, + "sympy_format": "dCl_tot_dt*(-0.11452771)*(Cl_err + Cl_tot + 1.4403551) + 0.78953165", + "lambda_format": "PySRFunction(X=>dCl_tot_dt*(-0.11452771)*(Cl_err + Cl_tot + 1.4403551) + 0.78953165)" + }, + { + "complexity": 15, + "loss": 0.013981272, + "equation": "(((square(Cl_err + 1.2575057) + Cl_tot) * -0.1052603) * dCl_tot_dt) + 0.76740843", + "score": 0.12315116683748104, + "sympy_format": "(Cl_tot + (Cl_err + 1.2575057)**2)*(-0.1052603)*dCl_tot_dt + 0.76740843", + "lambda_format": "PySRFunction(X=>(Cl_tot + (Cl_err + 1.2575057)**2)*(-0.1052603)*dCl_tot_dt + 0.76740843)" + } + ] +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_illusion_joint_front.json b/src/SR_analysis/results/archive/raw/pysr_illusion_joint_front.json new file mode 100644 index 0000000..b55c16c --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_illusion_joint_front.json @@ -0,0 +1,19 @@ +{ + "scene": "illusion_joint", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "Cd_err", + "Cl_err", + "dCd_err_dt", + "dCl_err_dt" + ], + "best_sympy": "Cd_tot - (Cd_err + 5.4276643) - (-0.009783858)*(du_a_dt + u_a)", + "best_score": 0.9072 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_illusion_joint_marker_front.json b/src/SR_analysis/results/archive/raw/pysr_illusion_joint_marker_front.json new file mode 100644 index 0000000..b079522 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_illusion_joint_marker_front.json @@ -0,0 +1,20 @@ +{ + "scene": "illusion_joint_marker", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "Cd_err", + "Cl_err", + "dCd_err_dt", + "dCl_err_dt", + "target_diameter" + ], + "best_sympy": "target_diameter*((du_a_dt + u_a)*0.010333712 + 6.324169) - 5.9960093", + "best_score": 0.9416 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_illusion_joint_top.json b/src/SR_analysis/results/archive/raw/pysr_illusion_joint_top.json new file mode 100644 index 0000000..b4f8b22 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_illusion_joint_top.json @@ -0,0 +1,20 @@ +{ + "scene": "illusion_joint", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "Cd_err", + "Cl_err", + "dCd_err_dt", + "dCl_err_dt" + ], + "best_sympy": "(Cd_err - (Cd_rear - Cl_err))*0.53502613 + 2.7819264", + "best_score": 0.8279 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re100_front.json b/src/SR_analysis/results/archive/raw/pysr_karman_re100_front.json new file mode 100644 index 0000000..15fcc97 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_karman_re100_front.json @@ -0,0 +1,90 @@ +{ + "scene": "karman_re100", + "scene_id": "karman", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear" + ], + "equations": [ + { + "complexity": 2, + "loss": 1.830206, + "equation": "-0.39158538", + "score": 0.0, + "sympy_format": "-0.391585380000000", + "lambda_format": "PySRFunction(X=>-0.391585380000000)" + }, + { + "complexity": 3, + "loss": 1.3063785, + "equation": "Cd_tot - Cl_tot", + "score": 0.3371697237256049, + "sympy_format": "Cd_tot - Cl_tot", + "lambda_format": "PySRFunction(X=>Cd_tot - Cl_tot)" + }, + { + "complexity": 4, + "loss": 0.23401318, + "equation": "u_a * -0.082187556", + "score": 1.7196366455226915, + "sympy_format": "u_a*(-0.082187556)", + "lambda_format": "PySRFunction(X=>u_a*(-0.082187556))" + }, + { + "complexity": 6, + "loss": 0.16904528, + "equation": "-0.07854679 * (u_a - Cd_rear)", + "score": 0.1626054152636259, + "sympy_format": "-0.07854679*(-Cd_rear + u_a)", + "lambda_format": "PySRFunction(X=>-0.07854679*(-Cd_rear + u_a))" + }, + { + "complexity": 7, + "loss": 0.1357354, + "equation": "(u_a - -3.875852) * -0.0810267", + "score": 0.21945920564510082, + "sympy_format": "(u_a - 1*(-3.875852))*(-0.0810267)", + "lambda_format": "PySRFunction(X=>(u_a - 1*(-3.875852))*(-0.0810267))" + }, + { + "complexity": 9, + "loss": 0.12549326, + "equation": "(u_a + (dCl_tot_dt + 3.9212615)) * -0.079753324", + "score": 0.039227675217555755, + "sympy_format": "(dCl_tot_dt + u_a + 3.9212615)*(-0.079753324)", + "lambda_format": "PySRFunction(X=>(dCl_tot_dt + u_a + 3.9212615)*(-0.079753324))" + }, + { + "complexity": 11, + "loss": 0.10655198, + "equation": "(u_a - ((Cd_rear - dCl_tot_dt) + -2.7390094)) * -0.07597935", + "score": 0.08180955537672423, + "sympy_format": "(u_a - (Cd_rear - dCl_tot_dt - 2.7390094))*(-0.07597935)", + "lambda_format": "PySRFunction(X=>(u_a - (Cd_rear - dCl_tot_dt - 2.7390094))*(-0.07597935))" + }, + { + "complexity": 12, + "loss": 0.09108441, + "equation": "((u_a + 4.0119615) * -0.078680456) - (du_a_dt * 0.01520837)", + "score": 0.15684628220122204, + "sympy_format": "-0.01520837*du_a_dt + (u_a + 4.0119615)*(-0.078680456)", + "lambda_format": "PySRFunction(X=>-0.01520837*du_a_dt + (u_a + 4.0119615)*(-0.078680456))" + }, + { + "complexity": 14, + "loss": 0.06762744, + "equation": "(u_a * -0.07061868) + ((du_a_dt * (Cd_rear * 0.01074081)) + -0.25213432)", + "score": 0.1488864205745558, + "sympy_format": "du_a_dt*Cd_rear*0.01074081 + u_a*(-0.07061868) - 0.25213432", + "lambda_format": "PySRFunction(X=>du_a_dt*Cd_rear*0.01074081 + u_a*(-0.07061868) - 0.25213432)" + } + ], + "best_sympy": "-0.01520837*du_a_dt + (u_a + 4.0119615)*(-0.078680456)", + "best_score": 0.9502327003605122 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re100_front_deep.json b/src/SR_analysis/results/archive/raw/pysr_karman_re100_front_deep.json new file mode 100644 index 0000000..9d85bc9 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_karman_re100_front_deep.json @@ -0,0 +1,27 @@ +{ + "scene": "karman_re100", + "scene_id": "karman", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "daF_dt", + "daB_dt", + "daT_dt", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "best_sympy": "mu_u_a*(-3.6490004) - (daF_dt - 1*2.3221624)/(Cl_diff - daF_dt)", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re100_mu_front.json b/src/SR_analysis/results/archive/raw/pysr_karman_re100_mu_front.json new file mode 100644 index 0000000..fb04dc2 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_karman_re100_mu_front.json @@ -0,0 +1,128 @@ +{ + "scene": "karman_re100", + "scene_id": "karman", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "equations": [ + { + "complexity": 1, + "loss": 1.868288, + "equation": "Cd_tot", + "score": 0.0, + "sympy_format": "Cd_tot", + "lambda_format": "PySRFunction(X=>Cd_tot)" + }, + { + "complexity": 2, + "loss": 1.830206, + "equation": "-0.39136213", + "score": 0.02059397468767714, + "sympy_format": "-0.391362130000000", + "lambda_format": "PySRFunction(X=>-0.391362130000000)" + }, + { + "complexity": 3, + "loss": 0.52802104, + "equation": "mu_u_a * Cd_rear", + "score": 1.2430476764089609, + "sympy_format": "Cd_rear*mu_u_a", + "lambda_format": "PySRFunction(X=>Cd_rear*mu_u_a)" + }, + { + "complexity": 4, + "loss": 0.23401318, + "equation": "mu_u_a * -4.109215", + "score": 0.8137586928393354, + "sympy_format": "mu_u_a*(-4.109215)", + "lambda_format": "PySRFunction(X=>mu_u_a*(-4.109215))" + }, + { + "complexity": 5, + "loss": 0.20410247, + "equation": "mu_u_a / (mu_Cl_diff + mu_v_a)", + "score": 0.13675526690444537, + "sympy_format": "mu_u_a/(mu_Cl_diff + mu_v_a)", + "lambda_format": "PySRFunction(X=>mu_u_a/(mu_Cl_diff + mu_v_a))" + }, + { + "complexity": 6, + "loss": 0.14800628, + "equation": "(mu_u_a * 0.67106664) / mu_Cl_diff", + "score": 0.32136746635708235, + "sympy_format": "mu_u_a*0.67106664/mu_Cl_diff", + "lambda_format": "PySRFunction(X=>mu_u_a*0.67106664/mu_Cl_diff)" + }, + { + "complexity": 7, + "loss": 0.1357354, + "equation": "(mu_u_a * -4.0513854) + -0.3140846", + "score": 0.08654730291082482, + "sympy_format": "mu_u_a*(-4.0513854) - 0.3140846", + "lambda_format": "PySRFunction(X=>mu_u_a*(-4.0513854) - 0.3140846)" + }, + { + "complexity": 8, + "loss": 0.098275356, + "equation": "((mu_u_a * 0.6663832) / mu_Cl_diff) + mu_Cl_diff", + "score": 0.3229341085962131, + "sympy_format": "mu_Cl_diff + mu_u_a*0.6663832/mu_Cl_diff", + "lambda_format": "PySRFunction(X=>mu_Cl_diff + mu_u_a*0.6663832/mu_Cl_diff)" + }, + { + "complexity": 9, + "loss": 0.08452758, + "equation": "(0.042753078 - (mu_u_a * -0.6482748)) / mu_Cl_diff", + "score": 0.15069542214740927, + "sympy_format": "(0.042753078 - (-0.6482748)*mu_u_a)/mu_Cl_diff", + "lambda_format": "PySRFunction(X=>(0.042753078 - (-0.6482748)*mu_u_a)/mu_Cl_diff)" + }, + { + "complexity": 11, + "loss": 0.082619995, + "equation": "(-0.6629376 / mu_Cl_diff) * ((-0.066422954 - mu_u_a) + mu_Cl_tot)", + "score": 0.011413075095643882, + "sympy_format": "-0.6629376*(mu_Cl_tot - mu_u_a - 0.066422954)/mu_Cl_diff", + "lambda_format": "PySRFunction(X=>-0.6629376*(mu_Cl_tot - mu_u_a - 0.066422954)/mu_Cl_diff)" + }, + { + "complexity": 12, + "loss": 0.07490997, + "equation": "square(mu_u_a) + ((0.06058765 - (mu_u_a * -0.65696585)) / mu_Cl_diff)", + "score": 0.09796472897144756, + "sympy_format": "mu_u_a**2 + (0.06058765 - (-0.65696585)*mu_u_a)/mu_Cl_diff", + "lambda_format": "PySRFunction(X=>mu_u_a**2 + (0.06058765 - (-0.65696585)*mu_u_a)/mu_Cl_diff)" + }, + { + "complexity": 14, + "loss": 0.07346657, + "equation": "square(mu_u_a) + (((0.06058765 - (mu_u_a * -0.65696585)) / mu_Cl_diff) + mu_Cl_tot)", + "score": 0.009728259829412203, + "sympy_format": "mu_Cl_tot + mu_u_a**2 + (0.06058765 - (-0.65696585)*mu_u_a)/mu_Cl_diff", + "lambda_format": "PySRFunction(X=>mu_Cl_tot + mu_u_a**2 + (0.06058765 - (-0.65696585)*mu_u_a)/mu_Cl_diff)" + }, + { + "complexity": 15, + "loss": 0.07342506, + "equation": "((0.06058765 - (mu_u_a * -0.65696585)) / mu_Cl_diff) + square(mu_u_a + square(mu_u_a))", + "score": 0.0005651785755926619, + "sympy_format": "(mu_u_a**2 + mu_u_a)**2 + (0.06058765 - (-0.65696585)*mu_u_a)/mu_Cl_diff", + "lambda_format": "PySRFunction(X=>(mu_u_a**2 + mu_u_a)**2 + (0.06058765 - (-0.65696585)*mu_u_a)/mu_Cl_diff)" + } + ], + "best_sympy": "mu_Cl_diff + mu_u_a*0.6663832/mu_Cl_diff", + "best_score": 0.946303670110334 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re100_mu_top.json b/src/SR_analysis/results/archive/raw/pysr_karman_re100_mu_top.json new file mode 100644 index 0000000..229dad5 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_karman_re100_mu_top.json @@ -0,0 +1,105 @@ +{ + "scene": "karman_re100", + "scene_id": "karman", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "equations": [ + { + "complexity": 1, + "loss": 12.876947, + "equation": "bias", + "score": 0.0, + "sympy_format": "bias", + "lambda_format": "PySRFunction(X=>bias)" + }, + { + "complexity": 2, + "loss": 3.1669734, + "equation": "4.116182", + "score": 1.4026622900123324, + "sympy_format": "4.11618200000000", + "lambda_format": "PySRFunction(X=>4.11618200000000)" + }, + { + "complexity": 4, + "loss": 2.9283972, + "equation": "mu_u_a + 4.0969486", + "score": 0.03916056288449243, + "sympy_format": "mu_u_a + 4.0969486", + "lambda_format": "PySRFunction(X=>mu_u_a + 4.0969486)" + }, + { + "complexity": 6, + "loss": 2.0342274, + "equation": "4.220499 - (mu_Cl_diff * du_a_dt)", + "score": 0.18216957592452662, + "sympy_format": "4.220499 - du_a_dt*mu_Cl_diff", + "lambda_format": "PySRFunction(X=>4.220499 - du_a_dt*mu_Cl_diff)" + }, + { + "complexity": 7, + "loss": 0.90240616, + "equation": "(du_a_dt * 0.1066316) - -4.1117396", + "score": 0.812806662807816, + "sympy_format": "du_a_dt*0.1066316 - 1*(-4.1117396)", + "lambda_format": "PySRFunction(X=>du_a_dt*0.1066316 - 1*(-4.1117396))" + }, + { + "complexity": 8, + "loss": 0.80963564, + "equation": "square(1.9872789 - (du_a_dt * -0.027672488))", + "score": 0.1084803876582273, + "sympy_format": "(1.9872789 - (-0.027672488)*du_a_dt)**2", + "lambda_format": "PySRFunction(X=>(1.9872789 - (-0.027672488)*du_a_dt)**2)" + }, + { + "complexity": 9, + "loss": 0.5986185, + "equation": "2.7047136 / (0.772592 - (mu * du_a_dt))", + "score": 0.3019598189163272, + "sympy_format": "2.7047136/(0.772592 - du_a_dt*mu)", + "lambda_format": "PySRFunction(X=>2.7047136/(0.772592 - du_a_dt*mu))" + }, + { + "complexity": 11, + "loss": 0.5384638, + "equation": "(2.7047136 / (0.772592 - (du_a_dt * mu))) + mu_u_a", + "score": 0.052952114909991126, + "sympy_format": "mu_u_a + 2.7047136/(0.772592 - du_a_dt*mu)", + "lambda_format": "PySRFunction(X=>mu_u_a + 2.7047136/(0.772592 - du_a_dt*mu))" + }, + { + "complexity": 13, + "loss": 0.44673786, + "equation": "(1.7194374 / (0.65746576 - (mu * (du_a_dt + Cd_rear)))) + bias", + "score": 0.0933741454657361, + "sympy_format": "bias + 1.7194374/(0.65746576 - mu*(Cd_rear + du_a_dt))", + "lambda_format": "PySRFunction(X=>bias + 1.7194374/(0.65746576 - mu*(Cd_rear + du_a_dt)))" + }, + { + "complexity": 15, + "loss": 0.26020092, + "equation": "mu_u_a + ((1.6812944 / (0.6521238 - ((du_a_dt + Cd_rear) * mu))) + bias)", + "score": 0.2702589389108651, + "sympy_format": "bias + mu_u_a + 1.6812944/(0.6521238 - mu*(Cd_rear + du_a_dt))", + "lambda_format": "PySRFunction(X=>bias + mu_u_a + 1.6812944/(0.6521238 - mu*(Cd_rear + du_a_dt)))" + } + ], + "best_sympy": "bias + mu_u_a + 1.6812944/(0.6521238 - mu*(Cd_rear + du_a_dt))", + "best_score": 0.9178392438881263 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re100_top.json b/src/SR_analysis/results/archive/raw/pysr_karman_re100_top.json new file mode 100644 index 0000000..a4b8668 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_karman_re100_top.json @@ -0,0 +1,115 @@ +{ + "scene": "karman_re100", + "scene_id": "karman", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear" + ], + "equations": [ + { + "complexity": 1, + "loss": 12.876947, + "equation": "bias", + "score": 0.0, + "sympy_format": "bias", + "lambda_format": "PySRFunction(X=>bias)" + }, + { + "complexity": 2, + "loss": 3.166973, + "equation": "4.1161165", + "score": 1.4026624163158958, + "sympy_format": "4.11611650000000", + "lambda_format": "PySRFunction(X=>4.11611650000000)" + }, + { + "complexity": 4, + "loss": 3.1042461, + "equation": "dCl_tot_dt + 4.084357", + "score": 0.01000267906493527, + "sympy_format": "dCl_tot_dt + 4.084357", + "lambda_format": "PySRFunction(X=>dCl_tot_dt + 4.084357)" + }, + { + "complexity": 6, + "loss": 2.8765476, + "equation": "(dCl_tot_dt + 4.2174454) - Cl_tot", + "score": 0.038090029449779524, + "sympy_format": "-Cl_tot + dCl_tot_dt + 4.2174454", + "lambda_format": "PySRFunction(X=>-Cl_tot + dCl_tot_dt + 4.2174454)" + }, + { + "complexity": 7, + "loss": 0.9024061, + "equation": "(du_a_dt * 0.10662521) + 4.1117325", + "score": 1.1592814635817714, + "sympy_format": "du_a_dt*0.10662521 + 4.1117325", + "lambda_format": "PySRFunction(X=>du_a_dt*0.10662521 + 4.1117325)" + }, + { + "complexity": 8, + "loss": 0.8096311, + "equation": "square((du_a_dt * 0.02769434) + 1.986891)", + "score": 0.1084859286456988, + "sympy_format": "(du_a_dt*0.02769434 + 1.986891)**2", + "lambda_format": "PySRFunction(X=>(du_a_dt*0.02769434 + 1.986891)**2)" + }, + { + "complexity": 9, + "loss": 0.67482555, + "equation": "((du_a_dt + Cd_rear) * 0.114317566) + 4.274448", + "score": 0.18212449880145706, + "sympy_format": "(Cd_rear + du_a_dt)*0.114317566 + 4.274448", + "lambda_format": "PySRFunction(X=>(Cd_rear + du_a_dt)*0.114317566 + 4.274448)" + }, + { + "complexity": 10, + "loss": 0.59065294, + "equation": "square(((Cd_rear + du_a_dt) * 0.028909246) + 2.026836)", + "score": 0.1332256100763366, + "sympy_format": "((Cd_rear + du_a_dt)*0.028909246 + 2.026836)**2", + "lambda_format": "PySRFunction(X=>((Cd_rear + du_a_dt)*0.028909246 + 2.026836)**2)" + }, + { + "complexity": 11, + "loss": 0.5532108, + "equation": "square(square(((Cd_rear + du_a_dt) * 0.010210121) + 1.4163154))", + "score": 0.06548948057261914, + "sympy_format": "((Cd_rear + du_a_dt)*0.010210121 + 1.4163154)**4", + "lambda_format": "PySRFunction(X=>((Cd_rear + du_a_dt)*0.010210121 + 1.4163154)**4)" + }, + { + "complexity": 12, + "loss": 0.50600797, + "equation": "square(((du_a_dt + (Cd_rear + Cd_rear)) * 0.02847228) + 2.068921)", + "score": 0.08918670222504659, + "sympy_format": "((Cd_rear + Cd_rear + du_a_dt)*0.02847228 + 2.068921)**2", + "lambda_format": "PySRFunction(X=>((Cd_rear + Cd_rear + du_a_dt)*0.02847228 + 2.068921)**2)" + }, + { + "complexity": 13, + "loss": 0.4443601, + "equation": "square(((Cd_rear + du_a_dt) * 0.06267052) + 0.8903389) + 2.722633", + "score": 0.12991715039530788, + "sympy_format": "((Cd_rear + du_a_dt)*0.06267052 + 0.8903389)**2 + 2.722633", + "lambda_format": "PySRFunction(X=>((Cd_rear + du_a_dt)*0.06267052 + 0.8903389)**2 + 2.722633)" + }, + { + "complexity": 14, + "loss": 0.43855473, + "equation": "square(square(((du_a_dt + Cd_rear) * 0.027539145) + 0.4846011) + 1.6436288)", + "score": 0.013150653873759735, + "sympy_format": "(((Cd_rear + du_a_dt)*0.027539145 + 0.4846011)**2 + 1.6436288)**2", + "lambda_format": "PySRFunction(X=>(((Cd_rear + du_a_dt)*0.027539145 + 0.4846011)**2 + 1.6436288)**2)" + } + ], + "best_sympy": "((Cd_rear + du_a_dt)*0.028909246 + 2.026836)**2", + "best_score": 0.8134960836704253 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re100_top_deep.json b/src/SR_analysis/results/archive/raw/pysr_karman_re100_top_deep.json new file mode 100644 index 0000000..7d760d1 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_karman_re100_top_deep.json @@ -0,0 +1,28 @@ +{ + "scene": "karman_re100", + "scene_id": "karman", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "daF_dt", + "daB_dt", + "daT_dt", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "best_sympy": "mu_u_a - (daF_dt + daT_dt*(-0.45103812)) + 4.084246", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re200_front.json b/src/SR_analysis/results/archive/raw/pysr_karman_re200_front.json new file mode 100644 index 0000000..ba23e8b --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_karman_re200_front.json @@ -0,0 +1,90 @@ +{ + "scene": "karman_re200", + "scene_id": "karman", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear" + ], + "equations": [ + { + "complexity": 1, + "loss": 18.199896, + "equation": "Cl_tot", + "score": 0.0, + "sympy_format": "Cl_tot", + "lambda_format": "PySRFunction(X=>Cl_tot)" + }, + { + "complexity": 2, + "loss": 5.2705092, + "equation": "0.48291796", + "score": 1.2392888995041884, + "sympy_format": "0.482917960000000", + "lambda_format": "PySRFunction(X=>0.482917960000000)" + }, + { + "complexity": 4, + "loss": 1.6048456, + "equation": "du_a_dt * -0.103824645", + "score": 0.5945497138494725, + "sympy_format": "du_a_dt*(-0.103824645)", + "lambda_format": "PySRFunction(X=>du_a_dt*(-0.103824645))" + }, + { + "complexity": 6, + "loss": 1.3081766, + "equation": "(du_a_dt + Cl_tot) * -0.10873986", + "score": 0.10219664668289626, + "sympy_format": "(Cl_tot + du_a_dt)*(-0.10873986)", + "lambda_format": "PySRFunction(X=>(Cl_tot + du_a_dt)*(-0.10873986))" + }, + { + "complexity": 8, + "loss": 1.1769742, + "equation": "((Cl_tot + Cd_rear) + du_a_dt) * -0.10710298", + "score": 0.05284367565491578, + "sympy_format": "(Cd_rear + Cl_tot + du_a_dt)*(-0.10710298)", + "lambda_format": "PySRFunction(X=>(Cd_rear + Cl_tot + du_a_dt)*(-0.10710298))" + }, + { + "complexity": 9, + "loss": 0.99413246, + "equation": "((du_a_dt + -6.524983) + Cd_tot) * -0.11263716", + "score": 0.1688317295484363, + "sympy_format": "(Cd_tot + du_a_dt - 6.524983)*(-0.11263716)", + "lambda_format": "PySRFunction(X=>(Cd_tot + du_a_dt - 6.524983)*(-0.11263716))" + }, + { + "complexity": 11, + "loss": 0.90825963, + "equation": "((du_a_dt + (Cd_tot + Cl_tot)) * -0.112961344) + 0.6465857", + "score": 0.04517009174147654, + "sympy_format": "0.6465857 + (Cd_tot + Cl_tot + du_a_dt)*(-0.112961344)", + "lambda_format": "PySRFunction(X=>0.6465857 + (Cd_tot + Cl_tot + du_a_dt)*(-0.112961344))" + }, + { + "complexity": 13, + "loss": 0.88926595, + "equation": "(((du_a_dt + (Cl_tot + Cd_tot)) + Cd_rear) * -0.108408295) + 0.6360827", + "score": 0.010566963360839525, + "sympy_format": "0.6360827 + (Cd_rear + Cd_tot + Cl_tot + du_a_dt)*(-0.108408295)", + "lambda_format": "PySRFunction(X=>0.6360827 + (Cd_rear + Cd_tot + Cl_tot + du_a_dt)*(-0.108408295))" + }, + { + "complexity": 14, + "loss": 0.8488093, + "equation": "((du_a_dt + (Cd_tot - (u_a * -0.19591688))) * -0.10738818) + 0.7137826", + "score": 0.04656180324659525, + "sympy_format": "0.7137826 + (Cd_tot + du_a_dt - (-0.19591688)*u_a)*(-0.10738818)", + "lambda_format": "PySRFunction(X=>0.7137826 + (Cd_tot + du_a_dt - (-0.19591688)*u_a)*(-0.10738818))" + } + ], + "best_sympy": "(Cd_tot + du_a_dt - 6.524983)*(-0.11263716)", + "best_score": 0.8113783188633168 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re200_front_deep.json b/src/SR_analysis/results/archive/raw/pysr_karman_re200_front_deep.json new file mode 100644 index 0000000..3ec04a9 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_karman_re200_front_deep.json @@ -0,0 +1,27 @@ +{ + "scene": "karman_re200", + "scene_id": "karman", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "daF_dt", + "daB_dt", + "daT_dt", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "best_sympy": "daF_dt - (-0.35635605)*(daB_dt + u_a/Cl_diff)", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re200_front_mu.json b/src/SR_analysis/results/archive/raw/pysr_karman_re200_front_mu.json new file mode 100644 index 0000000..9aa964e --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_karman_re200_front_mu.json @@ -0,0 +1,104 @@ +{ + "scene": "karman_re200", + "scene_id": "karman", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "equations": [ + { + "complexity": 1, + "loss": 5.4920754, + "equation": "mu_Cd_tot", + "score": 0.0, + "sympy_format": "mu_Cd_tot", + "lambda_format": "PySRFunction(X=>mu_Cd_tot)" + }, + { + "complexity": 2, + "loss": 5.2705097, + "equation": "0.48300102", + "score": 0.04117914172792375, + "sympy_format": "0.483001020000000", + "lambda_format": "PySRFunction(X=>0.483001020000000)" + }, + { + "complexity": 3, + "loss": 1.9170324, + "equation": "du_a_dt * mu_Cl_diff", + "score": 1.0113487098148788, + "sympy_format": "du_a_dt*mu_Cl_diff", + "lambda_format": "PySRFunction(X=>du_a_dt*mu_Cl_diff)" + }, + { + "complexity": 4, + "loss": 1.6048458, + "equation": "du_a_dt * -0.10382562", + "score": 0.1777506881289829, + "sympy_format": "du_a_dt*(-0.10382562)", + "lambda_format": "PySRFunction(X=>du_a_dt*(-0.10382562))" + }, + { + "complexity": 6, + "loss": 1.3081766, + "equation": "(Cl_tot + du_a_dt) * -0.10873986", + "score": 0.10219670899418266, + "sympy_format": "(Cl_tot + du_a_dt)*(-0.10873986)", + "lambda_format": "PySRFunction(X=>(Cl_tot + du_a_dt)*(-0.10873986))" + }, + { + "complexity": 8, + "loss": 1.1769743, + "equation": "((du_a_dt + Cl_tot) + Cd_rear) * -0.107102975", + "score": 0.05284363317310284, + "sympy_format": "(Cd_rear + Cl_tot + du_a_dt)*(-0.107102975)", + "lambda_format": "PySRFunction(X=>(Cd_rear + Cl_tot + du_a_dt)*(-0.107102975))" + }, + { + "complexity": 9, + "loss": 0.9941325, + "equation": "((du_a_dt + Cd_tot) + -6.524943) * -0.11263168", + "score": 0.16883177427597615, + "sympy_format": "(Cd_tot + du_a_dt - 6.524943)*(-0.11263168)", + "lambda_format": "PySRFunction(X=>(Cd_tot + du_a_dt - 6.524943)*(-0.11263168))" + }, + { + "complexity": 11, + "loss": 0.8417245, + "equation": "(mu_Cl_diff * ((du_a_dt + Cd_tot) + 22.292646)) + 3.0052881", + "score": 0.0832088670098608, + "sympy_format": "mu_Cl_diff*(Cd_tot + du_a_dt + 22.292646) + 3.0052881", + "lambda_format": "PySRFunction(X=>mu_Cl_diff*(Cd_tot + du_a_dt + 22.292646) + 3.0052881)" + }, + { + "complexity": 13, + "loss": 0.7899568, + "equation": "((Cd_tot + (du_a_dt + 19.366571)) * mu_Cl_diff) + (2.7221248 - mu_u_a)", + "score": 0.03173725156522626, + "sympy_format": "mu_Cl_diff*(Cd_tot + du_a_dt + 19.366571) - mu_u_a + 2.7221248", + "lambda_format": "PySRFunction(X=>mu_Cl_diff*(Cd_tot + du_a_dt + 19.366571) - mu_u_a + 2.7221248)" + }, + { + "complexity": 15, + "loss": 0.7653362, + "equation": "(2.4811091 - mu_u_a) + (((Cd_tot + Cl_tot) + (du_a_dt + 17.313627)) * mu_Cl_diff)", + "score": 0.015831523006316073, + "sympy_format": "mu_Cl_diff*(Cd_tot + Cl_tot + du_a_dt + 17.313627) - mu_u_a + 2.4811091", + "lambda_format": "PySRFunction(X=>mu_Cl_diff*(Cd_tot + Cl_tot + du_a_dt + 17.313627) - mu_u_a + 2.4811091)" + } + ], + "best_sympy": "(Cd_tot + du_a_dt - 6.524943)*(-0.11263168)", + "best_score": 0.8113783191533578 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re200_top.json b/src/SR_analysis/results/archive/raw/pysr_karman_re200_top.json new file mode 100644 index 0000000..fa3d710 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_karman_re200_top.json @@ -0,0 +1,99 @@ +{ + "scene": "karman_re200", + "scene_id": "karman", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear" + ], + "equations": [ + { + "complexity": 1, + "loss": 23.63484, + "equation": "bias", + "score": 0.0, + "sympy_format": "bias", + "lambda_format": "PySRFunction(X=>bias)" + }, + { + "complexity": 2, + "loss": 18.926178, + "equation": "3.170339", + "score": 0.22217585150789548, + "sympy_format": "3.17033900000000", + "lambda_format": "PySRFunction(X=>3.17033900000000)" + }, + { + "complexity": 6, + "loss": 15.155859, + "equation": "(du_a_dt * 0.15310828) + bias", + "score": 0.05553971330655655, + "sympy_format": "bias + du_a_dt*0.15310828", + "lambda_format": "PySRFunction(X=>bias + du_a_dt*0.15310828)" + }, + { + "complexity": 7, + "loss": 10.762966, + "equation": "(du_a_dt * 0.15028036) + 3.096591", + "score": 0.34227602256021533, + "sympy_format": "du_a_dt*0.15028036 + 3.096591", + "lambda_format": "PySRFunction(X=>du_a_dt*0.15028036 + 3.096591)" + }, + { + "complexity": 8, + "loss": 10.185801, + "equation": "square((du_a_dt * -0.05070497) - 1.4841495)", + "score": 0.05511647565394242, + "sympy_format": "(du_a_dt*(-0.05070497) - 1*1.4841495)**2", + "lambda_format": "PySRFunction(X=>(du_a_dt*(-0.05070497) - 1*1.4841495)**2)" + }, + { + "complexity": 9, + "loss": 9.516889, + "equation": "((u_a - du_a_dt) * -0.12908858) + 3.048999", + "score": 0.06792668196978036, + "sympy_format": "3.048999 + (-du_a_dt + u_a)*(-0.12908858)", + "lambda_format": "PySRFunction(X=>3.048999 + (-du_a_dt + u_a)*(-0.12908858))" + }, + { + "complexity": 10, + "loss": 9.355116, + "equation": "square(((Cd_tot + du_a_dt) * -0.049551576) - 1.4392014)", + "score": 0.017144650247514725, + "sympy_format": "((Cd_tot + du_a_dt)*(-0.049551576) - 1*1.4392014)**2", + "lambda_format": "PySRFunction(X=>((Cd_tot + du_a_dt)*(-0.049551576) - 1*1.4392014)**2)" + }, + { + "complexity": 11, + "loss": 7.8909245, + "equation": "(((du_a_dt - u_a) * 0.14840227) + Cd_rear) - -3.0669577", + "score": 0.1702100578043827, + "sympy_format": "Cd_rear + (du_a_dt - u_a)*0.14840227 - 1*(-3.0669577)", + "lambda_format": "PySRFunction(X=>Cd_rear + (du_a_dt - u_a)*0.14840227 - 1*(-3.0669577))" + }, + { + "complexity": 13, + "loss": 7.220813, + "equation": "(Cd_rear + (((Cl_tot + du_a_dt) - u_a) * 0.1584444)) - -3.1825032", + "score": 0.044372875593746954, + "sympy_format": "Cd_rear + (Cl_tot + du_a_dt - u_a)*0.1584444 - 1*(-3.1825032)", + "lambda_format": "PySRFunction(X=>Cd_rear + (Cl_tot + du_a_dt - u_a)*0.1584444 - 1*(-3.1825032))" + }, + { + "complexity": 14, + "loss": 6.414105, + "equation": "((((du_a_dt + Cd_tot) / 0.5196077) - u_a) * 0.09909016) + 2.6989543", + "score": 0.11846807886687814, + "sympy_format": "(-u_a + (Cd_tot + du_a_dt)/0.5196077)*0.09909016 + 2.6989543", + "lambda_format": "PySRFunction(X=>(-u_a + (Cd_tot + du_a_dt)/0.5196077)*0.09909016 + 2.6989543)" + } + ], + "best_sympy": "Cd_rear + (du_a_dt - u_a)*0.14840227 - 1*(-3.0669577)", + "best_score": 0.5830682812655203 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re200_top_deep.json b/src/SR_analysis/results/archive/raw/pysr_karman_re200_top_deep.json new file mode 100644 index 0000000..d607324 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_karman_re200_top_deep.json @@ -0,0 +1,28 @@ +{ + "scene": "karman_re200", + "scene_id": "karman", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "daF_dt", + "daB_dt", + "daT_dt", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "best_sympy": "-daB_dt*mu_u_a + daF_dt/(-0.77701634) + 2.6764865", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re200_top_mu.json b/src/SR_analysis/results/archive/raw/pysr_karman_re200_top_mu.json new file mode 100644 index 0000000..8befaa7 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_karman_re200_top_mu.json @@ -0,0 +1,121 @@ +{ + "scene": "karman_re200", + "scene_id": "karman", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "equations": [ + { + "complexity": 1, + "loss": 28.996807, + "equation": "mu_Cl_tot", + "score": 0.0, + "sympy_format": "mu_Cl_tot", + "lambda_format": "PySRFunction(X=>mu_Cl_tot)" + }, + { + "complexity": 2, + "loss": 18.926178, + "equation": "3.1703174", + "score": 0.42663967738330266, + "sympy_format": "3.17031740000000", + "lambda_format": "PySRFunction(X=>3.17031740000000)" + }, + { + "complexity": 4, + "loss": 17.197989, + "equation": "mu_Cl_diff * -34.770687", + "score": 0.0478767923569113, + "sympy_format": "mu_Cl_diff*(-34.770687)", + "lambda_format": "PySRFunction(X=>mu_Cl_diff*(-34.770687))" + }, + { + "complexity": 5, + "loss": 15.697181, + "equation": "bias - (du_a_dt * mu_Cl_diff)", + "score": 0.09131131628688273, + "sympy_format": "bias - du_a_dt*mu_Cl_diff", + "lambda_format": "PySRFunction(X=>bias - du_a_dt*mu_Cl_diff)" + }, + { + "complexity": 6, + "loss": 11.203202, + "equation": "(-31.053566 - du_a_dt) * mu_Cl_diff", + "score": 0.3372815117937443, + "sympy_format": "mu_Cl_diff*(-du_a_dt - 31.053566)", + "lambda_format": "PySRFunction(X=>mu_Cl_diff*(-du_a_dt - 31.053566))" + }, + { + "complexity": 7, + "loss": 10.762966, + "equation": "(du_a_dt * 0.15028043) + 3.096593", + "score": 0.04008846299199176, + "sympy_format": "du_a_dt*0.15028043 + 3.096593", + "lambda_format": "PySRFunction(X=>du_a_dt*0.15028043 + 3.096593)" + }, + { + "complexity": 8, + "loss": 7.768592, + "equation": "mu_Cl_diff * ((u_a + -33.898975) - du_a_dt)", + "score": 0.3260222291287542, + "sympy_format": "mu_Cl_diff*(-du_a_dt + u_a - 33.898975)", + "lambda_format": "PySRFunction(X=>mu_Cl_diff*(-du_a_dt + u_a - 33.898975))" + }, + { + "complexity": 10, + "loss": 7.2641587, + "equation": "(u_a + (-30.177534 - (du_a_dt + du_a_dt))) * mu_Cl_diff", + "score": 0.03356822482528503, + "sympy_format": "mu_Cl_diff*(u_a - (du_a_dt + du_a_dt) - 30.177534)", + "lambda_format": "PySRFunction(X=>mu_Cl_diff*(u_a - (du_a_dt + du_a_dt) - 30.177534))" + }, + { + "complexity": 11, + "loss": 6.6518455, + "equation": "(u_a + ((du_a_dt + 20.171255) * -1.5745826)) * mu_Cl_diff", + "score": 0.08805815356376312, + "sympy_format": "mu_Cl_diff*(u_a + (du_a_dt + 20.171255)*(-1.5745826))", + "lambda_format": "PySRFunction(X=>mu_Cl_diff*(u_a + (du_a_dt + 20.171255)*(-1.5745826)))" + }, + { + "complexity": 13, + "loss": 5.61977, + "equation": "mu_Cl_diff * (u_a + ((Cd_tot + (du_a_dt + 16.111593)) * -1.7283762))", + "score": 0.08430179858115139, + "sympy_format": "mu_Cl_diff*(u_a + (Cd_tot + du_a_dt + 16.111593)*(-1.7283762))", + "lambda_format": "PySRFunction(X=>mu_Cl_diff*(u_a + (Cd_tot + du_a_dt + 16.111593)*(-1.7283762)))" + }, + { + "complexity": 14, + "loss": 5.2487407, + "equation": "(((Cd_tot * 0.022970296) + 0.11651821) * (du_a_dt - u_a)) + 3.7002187", + "score": 0.06830255663697854, + "sympy_format": "(du_a_dt - u_a)*(Cd_tot*0.022970296 + 0.11651821) + 3.7002187", + "lambda_format": "PySRFunction(X=>(du_a_dt - u_a)*(Cd_tot*0.022970296 + 0.11651821) + 3.7002187)" + }, + { + "complexity": 15, + "loss": 4.803909, + "equation": "((u_a + -27.027966) * mu_Cl_diff) + ((du_a_dt + (Cd_tot + Cd_tot)) * 0.17895396)", + "score": 0.08855821967380768, + "sympy_format": "mu_Cl_diff*(u_a - 27.027966) + (Cd_tot + Cd_tot + du_a_dt)*0.17895396", + "lambda_format": "PySRFunction(X=>mu_Cl_diff*(u_a - 27.027966) + (Cd_tot + Cd_tot + du_a_dt)*0.17895396)" + } + ], + "best_sympy": "mu_Cl_diff*(u_a - 27.027966) + (Cd_tot + Cd_tot + du_a_dt)*0.17895396", + "best_score": 0.74617651578081 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re400_front.json b/src/SR_analysis/results/archive/raw/pysr_karman_re400_front.json new file mode 100644 index 0000000..91c53cd --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_karman_re400_front.json @@ -0,0 +1,98 @@ +{ + "scene": "karman_re400", + "scene_id": "karman", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear" + ], + "equations": [ + { + "complexity": 1, + "loss": 4.78508, + "equation": "Cd_tot", + "score": 0.0, + "sympy_format": "Cd_tot", + "lambda_format": "PySRFunction(X=>Cd_tot)" + }, + { + "complexity": 2, + "loss": 1.5217162, + "equation": "-0.01615569", + "score": 1.1456639668031254, + "sympy_format": "-0.0161556900000000", + "lambda_format": "PySRFunction(X=>-0.0161556900000000)" + }, + { + "complexity": 4, + "loss": 0.34880155, + "equation": "u_a * 0.052264873", + "score": 0.7365454600125749, + "sympy_format": "u_a*0.052264873", + "lambda_format": "PySRFunction(X=>u_a*0.052264873)" + }, + { + "complexity": 6, + "loss": 0.29023215, + "equation": "(Cl_tot + u_a) * 0.050345138", + "score": 0.09191100792671142, + "sympy_format": "(Cl_tot + u_a)*0.050345138", + "lambda_format": "PySRFunction(X=>(Cl_tot + u_a)*0.050345138)" + }, + { + "complexity": 8, + "loss": 0.2756929, + "equation": "((u_a + Cl_tot) + Cl_tot) * 0.047055725", + "score": 0.0256967774551955, + "sympy_format": "(Cl_tot + Cl_tot + u_a)*0.047055725", + "lambda_format": "PySRFunction(X=>(Cl_tot + Cl_tot + u_a)*0.047055725)" + }, + { + "complexity": 9, + "loss": 0.27556458, + "equation": "(u_a + (Cl_tot * 1.9082044)) * 0.047392014", + "score": 0.0004655537819375761, + "sympy_format": "(Cl_tot*1.9082044 + u_a)*0.047392014", + "lambda_format": "PySRFunction(X=>(Cl_tot*1.9082044 + u_a)*0.047392014)" + }, + { + "complexity": 10, + "loss": 0.2691284, + "equation": "0.046952475 * (Cl_tot + (Cl_tot + (Cd_tot + u_a)))", + "score": 0.023633422147211477, + "sympy_format": "0.046952475*(Cd_tot + Cl_tot + Cl_tot + u_a)", + "lambda_format": "PySRFunction(X=>0.046952475*(Cd_tot + Cl_tot + Cl_tot + u_a))" + }, + { + "complexity": 11, + "loss": 0.26674908, + "equation": "(Cl_tot + (u_a - (du_a_dt * 0.12980601))) * 0.052317653", + "score": 0.008880147885624036, + "sympy_format": "(Cl_tot - 0.12980601*du_a_dt + u_a)*0.052317653", + "lambda_format": "PySRFunction(X=>(Cl_tot - 0.12980601*du_a_dt + u_a)*0.052317653)" + }, + { + "complexity": 13, + "loss": 0.23537646, + "equation": "((Cl_tot + (Cl_tot + u_a)) - (du_a_dt * 0.18070638)) * 0.0498183", + "score": 0.0625611257139302, + "sympy_format": "(Cl_tot + Cl_tot - 0.18070638*du_a_dt + u_a)*0.0498183", + "lambda_format": "PySRFunction(X=>(Cl_tot + Cl_tot - 0.18070638*du_a_dt + u_a)*0.0498183)" + }, + { + "complexity": 15, + "loss": 0.22842969, + "equation": "(u_a + ((Cl_tot + Cl_tot) - ((du_a_dt + dCl_tot_dt) * 0.18335105))) * 0.049658217", + "score": 0.01497886462434244, + "sympy_format": "(Cl_tot + Cl_tot + u_a - 0.18335105*(dCl_tot_dt + du_a_dt))*0.049658217", + "lambda_format": "PySRFunction(X=>(Cl_tot + Cl_tot + u_a - 0.18335105*(dCl_tot_dt + du_a_dt))*0.049658217)" + } + ], + "best_sympy": "(Cl_tot + u_a)*0.050345138", + "best_score": 0.8092731834963454 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re400_front_deep.json b/src/SR_analysis/results/archive/raw/pysr_karman_re400_front_deep.json new file mode 100644 index 0000000..8569b0e --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_karman_re400_front_deep.json @@ -0,0 +1,27 @@ +{ + "scene": "karman_re400", + "scene_id": "karman", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "daF_dt", + "daB_dt", + "daT_dt", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "best_sympy": "mu_Cl_tot*21.972973 + mu_u_a*(Cl_diff - 1*(-18.038433))", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re400_front_mu.json b/src/SR_analysis/results/archive/raw/pysr_karman_re400_front_mu.json new file mode 100644 index 0000000..78d48d7 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_karman_re400_front_mu.json @@ -0,0 +1,112 @@ +{ + "scene": "karman_re400", + "scene_id": "karman", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "equations": [ + { + "complexity": 1, + "loss": 1.308247, + "equation": "mu_u_a", + "score": 0.0, + "sympy_format": "mu_u_a", + "lambda_format": "PySRFunction(X=>mu_u_a)" + }, + { + "complexity": 3, + "loss": 1.1159908, + "equation": "mu_u_a + mu_u_a", + "score": 0.07947272646515971, + "sympy_format": "mu_u_a + mu_u_a", + "lambda_format": "PySRFunction(X=>mu_u_a + mu_u_a)" + }, + { + "complexity": 4, + "loss": 0.34880155, + "equation": "mu_u_a * 10.452996", + "score": 1.1629947633473876, + "sympy_format": "mu_u_a*10.452996", + "lambda_format": "PySRFunction(X=>mu_u_a*10.452996)" + }, + { + "complexity": 6, + "loss": 0.29023215, + "equation": "(mu_Cl_tot + mu_u_a) / 0.09930771", + "score": 0.09191100792671142, + "sympy_format": "(mu_Cl_tot + mu_u_a)/0.09930771", + "lambda_format": "PySRFunction(X=>(mu_Cl_tot + mu_u_a)/0.09930771)" + }, + { + "complexity": 8, + "loss": 0.2756929, + "equation": "((mu_u_a + mu_Cl_tot) + mu_Cl_tot) / 0.10625569", + "score": 0.0256967774551955, + "sympy_format": "(mu_Cl_tot + mu_Cl_tot + mu_u_a)/0.10625569", + "lambda_format": "PySRFunction(X=>(mu_Cl_tot + mu_Cl_tot + mu_u_a)/0.10625569)" + }, + { + "complexity": 9, + "loss": 0.25127771, + "equation": "(mu_u_a + mu_Cl_tot) / (square(mu_u_a) + 0.07654974)", + "score": 0.09272882321931299, + "sympy_format": "(mu_Cl_tot + mu_u_a)/(mu_u_a**2 + 0.07654974)", + "lambda_format": "PySRFunction(X=>(mu_Cl_tot + mu_u_a)/(mu_u_a**2 + 0.07654974))" + }, + { + "complexity": 10, + "loss": 0.24833514, + "equation": "(mu_Cl_tot + (mu_Cl_tot + mu_u_a)) / (mu_Cl_diff * -2.4046078)", + "score": 0.011779536996392413, + "sympy_format": "(mu_Cl_tot + mu_Cl_tot + mu_u_a)/((mu_Cl_diff*(-2.4046078)))", + "lambda_format": "PySRFunction(X=>(mu_Cl_tot + mu_Cl_tot + mu_u_a)/((mu_Cl_diff*(-2.4046078))))" + }, + { + "complexity": 11, + "loss": 0.20570107, + "equation": "(mu_u_a + mu_Cl_tot) / (square(mu_u_a) + (mu_Cl_diff * -1.7357934))", + "score": 0.18835520645726708, + "sympy_format": "(mu_Cl_tot + mu_u_a)/(mu_Cl_diff*(-1.7357934) + mu_u_a**2)", + "lambda_format": "PySRFunction(X=>(mu_Cl_tot + mu_u_a)/(mu_Cl_diff*(-1.7357934) + mu_u_a**2))" + }, + { + "complexity": 13, + "loss": 0.18689093, + "equation": "((mu_u_a + mu_Cl_tot) + mu_Cl_tot) / (square(mu_u_a) + (mu_Cl_diff * -1.881803))", + "score": 0.04794940686823591, + "sympy_format": "(mu_Cl_tot + mu_Cl_tot + mu_u_a)/(mu_Cl_diff*(-1.881803) + mu_u_a**2)", + "lambda_format": "PySRFunction(X=>(mu_Cl_tot + mu_Cl_tot + mu_u_a)/(mu_Cl_diff*(-1.881803) + mu_u_a**2))" + }, + { + "complexity": 14, + "loss": 0.1845329, + "equation": "(mu_Cl_tot + (mu_u_a * 0.6425722)) / (square(mu_u_a) + (mu_Cl_diff * -1.0316918))", + "score": 0.012697417292335296, + "sympy_format": "(mu_Cl_tot + mu_u_a*0.6425722)/(mu_Cl_diff*(-1.0316918) + mu_u_a**2)", + "lambda_format": "PySRFunction(X=>(mu_Cl_tot + mu_u_a*0.6425722)/(mu_Cl_diff*(-1.0316918) + mu_u_a**2))" + }, + { + "complexity": 15, + "loss": 0.1790493, + "equation": "((mu_Cl_tot + (mu_u_a + mu_Cl_tot)) / (square(mu_u_a) + (mu_Cl_diff * -1.881803))) - mu_v_a", + "score": 0.03016658045237722, + "sympy_format": "-mu_v_a + (mu_Cl_tot + mu_Cl_tot + mu_u_a)/(mu_Cl_diff*(-1.881803) + mu_u_a**2)", + "lambda_format": "PySRFunction(X=>-mu_v_a + (mu_Cl_tot + mu_Cl_tot + mu_u_a)/(mu_Cl_diff*(-1.881803) + mu_u_a**2))" + } + ], + "best_sympy": "(mu_Cl_tot + mu_u_a)/(mu_Cl_diff*(-1.7357934) + mu_u_a**2)", + "best_score": 0.8648229866954946 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re400_top.json b/src/SR_analysis/results/archive/raw/pysr_karman_re400_top.json new file mode 100644 index 0000000..03e54fe --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_karman_re400_top.json @@ -0,0 +1,99 @@ +{ + "scene": "karman_re400", + "scene_id": "karman", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear" + ], + "equations": [ + { + "complexity": 1, + "loss": 12.389752, + "equation": "bias", + "score": 0.0, + "sympy_format": "bias", + "lambda_format": "PySRFunction(X=>bias)" + }, + { + "complexity": 2, + "loss": 9.910323, + "equation": "2.5744174", + "score": 0.22329273814786962, + "sympy_format": "2.57441740000000", + "lambda_format": "PySRFunction(X=>2.57441740000000)" + }, + { + "complexity": 3, + "loss": 9.031403, + "equation": "bias - Cl_tot", + "score": 0.09286921480512979, + "sympy_format": "-Cl_tot + bias", + "lambda_format": "PySRFunction(X=>-Cl_tot + bias)" + }, + { + "complexity": 4, + "loss": 6.3928876, + "equation": "2.6243637 - Cl_tot", + "score": 0.34552166644566396, + "sympy_format": "2.6243637 - Cl_tot", + "lambda_format": "PySRFunction(X=>2.6243637 - Cl_tot)" + }, + { + "complexity": 6, + "loss": 6.392887, + "equation": "(bias + 1.6244478) - Cl_tot", + "score": 4.692715292301224e-08, + "sympy_format": "-Cl_tot + bias + 1.6244478", + "lambda_format": "PySRFunction(X=>-Cl_tot + bias + 1.6244478)" + }, + { + "complexity": 7, + "loss": 4.8074017, + "equation": "(u_a * -0.10901154) + 2.5849853", + "score": 0.2850292149918444, + "sympy_format": "2.5849853 + u_a*(-0.10901154)", + "lambda_format": "PySRFunction(X=>2.5849853 + u_a*(-0.10901154))" + }, + { + "complexity": 9, + "loss": 3.5161612, + "equation": "((du_a_dt + u_a) * -0.07302963) - -2.574453", + "score": 0.15639346236392973, + "sympy_format": "(du_a_dt + u_a)*(-0.07302963) - 1*(-2.574453)", + "lambda_format": "PySRFunction(X=>(du_a_dt + u_a)*(-0.07302963) - 1*(-2.574453))" + }, + { + "complexity": 11, + "loss": 3.1495004, + "equation": "(((u_a + du_a_dt) + Cl_tot) * -0.07160907) + 2.5780187", + "score": 0.055062994620938645, + "sympy_format": "2.5780187 + (Cl_tot + du_a_dt + u_a)*(-0.07160907)", + "lambda_format": "PySRFunction(X=>2.5780187 + (Cl_tot + du_a_dt + u_a)*(-0.07160907))" + }, + { + "complexity": 12, + "loss": 2.70779, + "equation": "((u_a * -0.08291036) + (Cl_tot * -0.48432234)) + 2.6066887", + "score": 0.1511110330470069, + "sympy_format": "Cl_tot*(-0.48432234) + u_a*(-0.08291036) + 2.6066887", + "lambda_format": "PySRFunction(X=>Cl_tot*(-0.48432234) + u_a*(-0.08291036) + 2.6066887)" + }, + { + "complexity": 13, + "loss": 2.632371, + "equation": "(u_a * -0.08262691) + square((Cl_tot * 0.15481663) + -1.5408969)", + "score": 0.0282478430608063, + "sympy_format": "u_a*(-0.08262691) + (Cl_tot*0.15481663 - 1.5408969)**2", + "lambda_format": "PySRFunction(X=>u_a*(-0.08262691) + (Cl_tot*0.15481663 - 1.5408969)**2)" + } + ], + "best_sympy": "(du_a_dt + u_a)*(-0.07302963) - 1*(-2.574453)", + "best_score": 0.6452021838593329 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re400_top_deep.json b/src/SR_analysis/results/archive/raw/pysr_karman_re400_top_deep.json new file mode 100644 index 0000000..f6e5555 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_karman_re400_top_deep.json @@ -0,0 +1,28 @@ +{ + "scene": "karman_re400", + "scene_id": "karman", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "daF_dt", + "daB_dt", + "daT_dt", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "best_sympy": "-daF_dt + u_a*(-0.073740624) + 2.584631", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re400_top_mu.json b/src/SR_analysis/results/archive/raw/pysr_karman_re400_top_mu.json new file mode 100644 index 0000000..ffe64f5 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_karman_re400_top_mu.json @@ -0,0 +1,113 @@ +{ + "scene": "karman_re400", + "scene_id": "karman", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "equations": [ + { + "complexity": 1, + "loss": 12.389752, + "equation": "bias", + "score": 0.0, + "sympy_format": "bias", + "lambda_format": "PySRFunction(X=>bias)" + }, + { + "complexity": 2, + "loss": 9.910323, + "equation": "2.574628", + "score": 0.22329273814786962, + "sympy_format": "2.57462800000000", + "lambda_format": "PySRFunction(X=>2.57462800000000)" + }, + { + "complexity": 3, + "loss": 9.031403, + "equation": "bias - Cl_tot", + "score": 0.09286921480512979, + "sympy_format": "-Cl_tot + bias", + "lambda_format": "PySRFunction(X=>-Cl_tot + bias)" + }, + { + "complexity": 4, + "loss": 6.392887, + "equation": "2.6243238 - Cl_tot", + "score": 0.3455217602999697, + "sympy_format": "2.6243238 - Cl_tot", + "lambda_format": "PySRFunction(X=>2.6243238 - Cl_tot)" + }, + { + "complexity": 6, + "loss": 4.923951, + "equation": "(mu_u_a / mu_Cl_diff) + 2.578895", + "score": 0.13053735451607612, + "sympy_format": "2.578895 + mu_u_a/mu_Cl_diff", + "lambda_format": "PySRFunction(X=>2.578895 + mu_u_a/mu_Cl_diff)" + }, + { + "complexity": 7, + "loss": 4.8074017, + "equation": "2.5849957 - (mu_u_a * 21.800882)", + "score": 0.023954505959692236, + "sympy_format": "2.5849957 - 21.800882*mu_u_a", + "lambda_format": "PySRFunction(X=>2.5849957 - 21.800882*mu_u_a)" + }, + { + "complexity": 9, + "loss": 3.9020126, + "equation": "(20.402802 - Cl_tot) * (0.1462855 - mu_u_a)", + "score": 0.10433213987244033, + "sympy_format": "(0.1462855 - mu_u_a)*(20.402802 - Cl_tot)", + "lambda_format": "PySRFunction(X=>(0.1462855 - mu_u_a)*(20.402802 - Cl_tot))" + }, + { + "complexity": 10, + "loss": 3.3132908, + "equation": "(mu_Cl_diff * du_a_dt) + ((mu_u_a / mu_Cl_diff) - -2.4627469)", + "score": 0.16355057641313628, + "sympy_format": "du_a_dt*mu_Cl_diff - 1*(-2.4627469) + mu_u_a/mu_Cl_diff", + "lambda_format": "PySRFunction(X=>du_a_dt*mu_Cl_diff - 1*(-2.4627469) + mu_u_a/mu_Cl_diff)" + }, + { + "complexity": 11, + "loss": 2.8072584, + "equation": "(Cl_tot * -0.45563862) + ((mu_u_a / mu_Cl_diff) - -2.6016932)", + "score": 0.16573354617760666, + "sympy_format": "Cl_tot*(-0.45563862) - 1*(-2.6016932) + mu_u_a/mu_Cl_diff", + "lambda_format": "PySRFunction(X=>Cl_tot*(-0.45563862) - 1*(-2.6016932) + mu_u_a/mu_Cl_diff)" + }, + { + "complexity": 12, + "loss": 2.689544, + "equation": "((0.47283006 - mu_u_a) * (17.07065 - Cl_tot)) + -5.58099", + "score": 0.042836686202699564, + "sympy_format": "(0.47283006 - mu_u_a)*(17.07065 - Cl_tot) - 5.58099", + "lambda_format": "PySRFunction(X=>(0.47283006 - mu_u_a)*(17.07065 - Cl_tot) - 5.58099)" + }, + { + "complexity": 13, + "loss": 2.4883745, + "equation": "(Cl_tot * -0.45561153) + ((mu_u_a / (mu_Cl_diff - mu)) + 2.6015983)", + "score": 0.0777419764579431, + "sympy_format": "Cl_tot*(-0.45561153) + mu_u_a/(-mu + mu_Cl_diff) + 2.6015983", + "lambda_format": "PySRFunction(X=>Cl_tot*(-0.45561153) + mu_u_a/(-mu + mu_Cl_diff) + 2.6015983)" + } + ], + "best_sympy": "Cl_tot*(-0.45563862) - 1*(-2.6016932) + mu_u_a/mu_Cl_diff", + "best_score": 0.7167339058496913 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re50_front.json b/src/SR_analysis/results/archive/raw/pysr_karman_re50_front.json new file mode 100644 index 0000000..2f33f2c --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_karman_re50_front.json @@ -0,0 +1,90 @@ +{ + "scene": "karman_re50", + "scene_id": "karman", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear" + ], + "equations": [ + { + "complexity": 1, + "loss": 2.9416304, + "equation": "Cd_rear", + "score": 0.0, + "sympy_format": "Cd_rear", + "lambda_format": "PySRFunction(X=>Cd_rear)" + }, + { + "complexity": 3, + "loss": 1.7490981, + "equation": "Cd_rear - Cd_tot", + "score": 0.25993185090300236, + "sympy_format": "Cd_rear - Cd_tot", + "lambda_format": "PySRFunction(X=>Cd_rear - Cd_tot)" + }, + { + "complexity": 4, + "loss": 0.79536694, + "equation": "Cl_tot * -0.82232404", + "score": 0.7880519947226632, + "sympy_format": "Cl_tot*(-0.82232404)", + "lambda_format": "PySRFunction(X=>Cl_tot*(-0.82232404))" + }, + { + "complexity": 5, + "loss": 0.7668625, + "equation": "(Cd_rear * Cd_tot) - Cl_tot", + "score": 0.036496052502993634, + "sympy_format": "Cd_rear*Cd_tot - Cl_tot", + "lambda_format": "PySRFunction(X=>Cd_rear*Cd_tot - Cl_tot)" + }, + { + "complexity": 6, + "loss": 0.49224868, + "equation": "(Cl_tot + Cd_tot) * -0.72686523", + "score": 0.4433234794624367, + "sympy_format": "(Cd_tot + Cl_tot)*(-0.72686523)", + "lambda_format": "PySRFunction(X=>(Cd_tot + Cl_tot)*(-0.72686523))" + }, + { + "complexity": 9, + "loss": 0.43290463, + "equation": "(0.3690145 - (Cl_tot + Cd_tot)) * 0.7874215", + "score": 0.042822195416652616, + "sympy_format": "(0.3690145 - (Cd_tot + Cl_tot))*0.7874215", + "lambda_format": "PySRFunction(X=>(0.3690145 - (Cd_tot + Cl_tot))*0.7874215)" + }, + { + "complexity": 11, + "loss": 0.34404337, + "equation": "(u_a / 9.730794) - ((Cd_tot + Cl_tot) * 0.5569139)", + "score": 0.11487486234675012, + "sympy_format": "u_a/9.730794 - 0.5569139*(Cd_tot + Cl_tot)", + "lambda_format": "PySRFunction(X=>u_a/9.730794 - 0.5569139*(Cd_tot + Cl_tot))" + }, + { + "complexity": 13, + "loss": 0.3249568, + "equation": "(u_a / (Cl_tot + 9.305581)) - ((Cd_tot + Cl_tot) * 0.5823044)", + "score": 0.028537737295088166, + "sympy_format": "u_a/(Cl_tot + 9.305581) - 0.5823044*(Cd_tot + Cl_tot)", + "lambda_format": "PySRFunction(X=>u_a/(Cl_tot + 9.305581) - 0.5823044*(Cd_tot + Cl_tot))" + }, + { + "complexity": 14, + "loss": 0.30044007, + "equation": "(u_a / (square(Cl_tot) + 7.598077)) - ((Cl_tot + Cd_tot) * 0.614451)", + "score": 0.07844395060836307, + "sympy_format": "u_a/(Cl_tot**2 + 7.598077) - 0.614451*(Cd_tot + Cl_tot)", + "lambda_format": "PySRFunction(X=>u_a/(Cl_tot**2 + 7.598077) - 0.614451*(Cd_tot + Cl_tot))" + } + ], + "best_sympy": "u_a/9.730794 - 0.5569139*(Cd_tot + Cl_tot)", + "best_score": 0.899220387771316 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re50_front_deep.json b/src/SR_analysis/results/archive/raw/pysr_karman_re50_front_deep.json new file mode 100644 index 0000000..a081102 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_karman_re50_front_deep.json @@ -0,0 +1,27 @@ +{ + "scene": "karman_re50", + "scene_id": "karman", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "daF_dt", + "daB_dt", + "daT_dt", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "best_sympy": "Cd_rear*Cd_tot - 0.5113535*Cl_tot + mu_u_a*(2.8397622 - daB_dt)", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re50_front_mu.json b/src/SR_analysis/results/archive/raw/pysr_karman_re50_front_mu.json new file mode 100644 index 0000000..723bcf8 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_karman_re50_front_mu.json @@ -0,0 +1,104 @@ +{ + "scene": "karman_re50", + "scene_id": "karman", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "equations": [ + { + "complexity": 1, + "loss": 2.9416304, + "equation": "Cd_rear", + "score": 0.0, + "sympy_format": "Cd_rear", + "lambda_format": "PySRFunction(X=>Cd_rear)" + }, + { + "complexity": 3, + "loss": 0.8881686, + "equation": "mu_Cl_tot - Cl_tot", + "score": 0.5987788373052547, + "sympy_format": "-Cl_tot + mu_Cl_tot", + "lambda_format": "PySRFunction(X=>-Cl_tot + mu_Cl_tot)" + }, + { + "complexity": 4, + "loss": 0.79536694, + "equation": "Cl_tot * -0.82232404", + "score": 0.11035802191815856, + "sympy_format": "Cl_tot*(-0.82232404)", + "lambda_format": "PySRFunction(X=>Cl_tot*(-0.82232404))" + }, + { + "complexity": 5, + "loss": 0.6453504, + "equation": "(Cl_tot - u_a) * mu_Cl_diff", + "score": 0.20901014281711638, + "sympy_format": "mu_Cl_diff*(Cl_tot - u_a)", + "lambda_format": "PySRFunction(X=>mu_Cl_diff*(Cl_tot - u_a))" + }, + { + "complexity": 6, + "loss": 0.4933364, + "equation": "-0.7142539 * (Cl_tot + Cd_tot)", + "score": 0.26860213081276096, + "sympy_format": "-0.7142539*(Cd_tot + Cl_tot)", + "lambda_format": "PySRFunction(X=>-0.7142539*(Cd_tot + Cl_tot))" + }, + { + "complexity": 8, + "loss": 0.42380178, + "equation": "((mu_u_a - Cl_tot) - Cd_tot) * 0.6871542", + "score": 0.07596272418359207, + "sympy_format": "(-Cd_tot - Cl_tot + mu_u_a)*0.6871542", + "lambda_format": "PySRFunction(X=>(-Cd_tot - Cl_tot + mu_u_a)*0.6871542)" + }, + { + "complexity": 10, + "loss": 0.37326324, + "equation": "(0.65648246 * ((mu_u_a - Cl_tot) - Cd_tot)) + mu_u_a", + "score": 0.06349096896298477, + "sympy_format": "mu_u_a + 0.65648246*(-Cd_tot - Cl_tot + mu_u_a)", + "lambda_format": "PySRFunction(X=>mu_u_a + 0.65648246*(-Cd_tot - Cl_tot + mu_u_a))" + }, + { + "complexity": 12, + "loss": 0.35045066, + "equation": "mu_u_a + ((((mu_u_a - Cl_tot) - Cd_tot) - mu_Cl_diff) * 0.65648246)", + "score": 0.03153199087816169, + "sympy_format": "mu_u_a + (-Cd_tot - Cl_tot - mu_Cl_diff + mu_u_a)*0.65648246", + "lambda_format": "PySRFunction(X=>mu_u_a + (-Cd_tot - Cl_tot - mu_Cl_diff + mu_u_a)*0.65648246)" + }, + { + "complexity": 13, + "loss": 0.34188312, + "equation": "(mu_u_a + ((Cl_tot + (Cd_tot - mu_u_a)) * (mu_Cl_diff + mu_Cl_diff))) + mu_u_a", + "score": 0.02475100197216812, + "sympy_format": "mu_u_a + mu_u_a + (mu_Cl_diff + mu_Cl_diff)*(Cd_tot + Cl_tot - mu_u_a)", + "lambda_format": "PySRFunction(X=>mu_u_a + mu_u_a + (mu_Cl_diff + mu_Cl_diff)*(Cd_tot + Cl_tot - mu_u_a))" + }, + { + "complexity": 15, + "loss": 0.3383513, + "equation": "(mu_u_a + ((((Cl_tot + Cd_tot) - mu_u_a) + mu_Cl_diff) * (mu_Cl_diff + mu_Cl_diff))) + mu_u_a", + "score": 0.005192109709592868, + "sympy_format": "mu_u_a + mu_u_a + (mu_Cl_diff + mu_Cl_diff)*(Cd_tot + Cl_tot + mu_Cl_diff - mu_u_a)", + "lambda_format": "PySRFunction(X=>mu_u_a + mu_u_a + (mu_Cl_diff + mu_Cl_diff)*(Cd_tot + Cl_tot + mu_Cl_diff - mu_u_a))" + } + ], + "best_sympy": "-0.7142539*(Cd_tot + Cl_tot)", + "best_score": 0.855488440720928 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re50_top.json b/src/SR_analysis/results/archive/raw/pysr_karman_re50_top.json new file mode 100644 index 0000000..84521da --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_karman_re50_top.json @@ -0,0 +1,83 @@ +{ + "scene": "karman_re50", + "scene_id": "karman", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear" + ], + "equations": [ + { + "complexity": 1, + "loss": 7.3960657, + "equation": "bias", + "score": 0.0, + "sympy_format": "bias", + "lambda_format": "PySRFunction(X=>bias)" + }, + { + "complexity": 2, + "loss": 0.7250843, + "equation": "3.5828395", + "score": 2.322415551690428, + "sympy_format": "3.58283950000000", + "lambda_format": "PySRFunction(X=>3.58283950000000)" + }, + { + "complexity": 4, + "loss": 0.40595657, + "equation": "3.8349104 - Cd_tot", + "score": 0.2900208702602963, + "sympy_format": "3.8349104 - Cd_tot", + "lambda_format": "PySRFunction(X=>3.8349104 - Cd_tot)" + }, + { + "complexity": 6, + "loss": 0.18479405, + "equation": "(Cd_rear - Cd_tot) + 4.2190733", + "score": 0.39350211085854186, + "sympy_format": "Cd_rear - Cd_tot + 4.2190733", + "lambda_format": "PySRFunction(X=>Cd_rear - Cd_tot + 4.2190733)" + }, + { + "complexity": 9, + "loss": 0.16348135, + "equation": "((Cd_tot - Cd_rear) * -0.83695704) + 4.115304", + "score": 0.04084768172223313, + "sympy_format": "4.115304 + (-Cd_rear + Cd_tot)*(-0.83695704)", + "lambda_format": "PySRFunction(X=>4.115304 + (-Cd_rear + Cd_tot)*(-0.83695704))" + }, + { + "complexity": 12, + "loss": 0.15617849, + "equation": "((square(Cd_rear) * 0.2508391) + (Cd_rear - Cd_tot)) + 4.0916963", + "score": 0.01523313223215865, + "sympy_format": "Cd_rear**2*0.2508391 + Cd_rear - Cd_tot + 4.0916963", + "lambda_format": "PySRFunction(X=>Cd_rear**2*0.2508391 + Cd_rear - Cd_tot + 4.0916963)" + }, + { + "complexity": 14, + "loss": 0.15617847, + "equation": "(((square(Cd_rear + Cd_rear) * 0.06271006) + 4.091696) + Cd_rear) - Cd_tot", + "score": 6.40293079587111e-08, + "sympy_format": "Cd_rear - Cd_tot + (Cd_rear + Cd_rear)**2*0.06271006 + 4.091696", + "lambda_format": "PySRFunction(X=>Cd_rear - Cd_tot + (Cd_rear + Cd_rear)**2*0.06271006 + 4.091696)" + }, + { + "complexity": 15, + "loss": 0.15617846, + "equation": "((square(Cd_rear) * 0.25083873) + ((Cd_rear + 4.355814) - Cd_tot)) - 0.2641176", + "score": 6.40293143465305e-08, + "sympy_format": "Cd_rear**2*0.25083873 + Cd_rear - Cd_tot - 1*0.2641176 + 4.355814", + "lambda_format": "PySRFunction(X=>Cd_rear**2*0.25083873 + Cd_rear - Cd_tot - 1*0.2641176 + 4.355814)" + } + ], + "best_sympy": "Cd_rear - Cd_tot + 4.2190733", + "best_score": 0.7451412793338374 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re50_top_deep.json b/src/SR_analysis/results/archive/raw/pysr_karman_re50_top_deep.json new file mode 100644 index 0000000..2e5577f --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_karman_re50_top_deep.json @@ -0,0 +1,28 @@ +{ + "scene": "karman_re50", + "scene_id": "karman", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "daF_dt", + "daB_dt", + "daT_dt", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "best_sympy": "(0.74166375 - (-0.0649042)*daT_dt)*(Cd_rear - Cd_tot + 5.43748)", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/pysr_karman_re50_top_mu.json b/src/SR_analysis/results/archive/raw/pysr_karman_re50_top_mu.json new file mode 100644 index 0000000..101cc55 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/pysr_karman_re50_top_mu.json @@ -0,0 +1,113 @@ +{ + "scene": "karman_re50", + "scene_id": "karman", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "equations": [ + { + "complexity": 1, + "loss": 7.3960657, + "equation": "bias", + "score": 0.0, + "sympy_format": "bias", + "lambda_format": "PySRFunction(X=>bias)" + }, + { + "complexity": 2, + "loss": 0.72508425, + "equation": "3.5827675", + "score": 2.3224156206479294, + "sympy_format": "3.58276750000000", + "lambda_format": "PySRFunction(X=>3.58276750000000)" + }, + { + "complexity": 4, + "loss": 0.40595657, + "equation": "3.8348339 - Cd_tot", + "score": 0.2900208357815456, + "sympy_format": "3.8348339 - Cd_tot", + "lambda_format": "PySRFunction(X=>3.8348339 - Cd_tot)" + }, + { + "complexity": 6, + "loss": 0.18479405, + "equation": "Cd_rear + (4.2190323 - Cd_tot)", + "score": 0.39350211085854186, + "sympy_format": "Cd_rear - Cd_tot + 4.2190323", + "lambda_format": "PySRFunction(X=>Cd_rear - Cd_tot + 4.2190323)" + }, + { + "complexity": 8, + "loss": 0.16480848, + "equation": "((Cd_rear + 4.1410832) - mu_v_a) - Cd_tot", + "score": 0.05722894462866641, + "sympy_format": "Cd_rear - Cd_tot - mu_v_a + 4.1410832", + "lambda_format": "PySRFunction(X=>Cd_rear - Cd_tot - mu_v_a + 4.1410832)" + }, + { + "complexity": 9, + "loss": 0.16348134, + "equation": "((Cd_rear - Cd_tot) / 1.1948733) + 4.115267", + "score": 0.008085217078425343, + "sympy_format": "(Cd_rear - Cd_tot)/1.1948733 + 4.115267", + "lambda_format": "PySRFunction(X=>(Cd_rear - Cd_tot)/1.1948733 + 4.115267)" + }, + { + "complexity": 10, + "loss": 0.14175749, + "equation": "((4.1139817 - Cd_tot) + (Cd_rear - mu_v_a)) - mu_u_a", + "score": 0.1425810746657183, + "sympy_format": "Cd_rear - Cd_tot - mu_u_a - mu_v_a + 4.1139817", + "lambda_format": "PySRFunction(X=>Cd_rear - Cd_tot - mu_u_a - mu_v_a + 4.1139817)" + }, + { + "complexity": 11, + "loss": 0.113665275, + "equation": "((Cd_rear - Cd_tot) / 1.3379613) + (3.9803717 - mu_v_a)", + "score": 0.22085983557938038, + "sympy_format": "-mu_v_a + (Cd_rear - Cd_tot)/1.3379613 + 3.9803717", + "lambda_format": "PySRFunction(X=>-mu_v_a + (Cd_rear - Cd_tot)/1.3379613 + 3.9803717)" + }, + { + "complexity": 13, + "loss": 0.110659674, + "equation": "((Cd_rear - Cd_tot) / (1.3713312 - mu_Cd_tot)) + (3.9827664 - mu_v_a)", + "score": 0.013399226816836118, + "sympy_format": "-mu_v_a + 3.9827664 + (Cd_rear - Cd_tot)/(1.3713312 - mu_Cd_tot)", + "lambda_format": "PySRFunction(X=>-mu_v_a + 3.9827664 + (Cd_rear - Cd_tot)/(1.3713312 - mu_Cd_tot))" + }, + { + "complexity": 14, + "loss": 0.10633017, + "equation": "(((Cd_rear - Cd_tot) / 1.3644128) + 3.92127) - (mu_v_a - square(mu_v_a))", + "score": 0.03991042704710708, + "sympy_format": "(Cd_rear - Cd_tot)/1.3644128 - (-mu_v_a**2 + mu_v_a) + 3.92127", + "lambda_format": "PySRFunction(X=>(Cd_rear - Cd_tot)/1.3644128 - (-mu_v_a**2 + mu_v_a) + 3.92127)" + }, + { + "complexity": 15, + "loss": 0.088840164, + "equation": "(((Cd_rear - Cd_tot) / 1.3713312) - (mu_Cl_tot * dCl_tot_dt)) + (3.9827664 - mu_v_a)", + "score": 0.1797102194180861, + "sympy_format": "-dCl_tot_dt*mu_Cl_tot - mu_v_a + (Cd_rear - Cd_tot)/1.3713312 + 3.9827664", + "lambda_format": "PySRFunction(X=>-dCl_tot_dt*mu_Cl_tot - mu_v_a + (Cd_rear - Cd_tot)/1.3713312 + 3.9827664)" + } + ], + "best_sympy": "-mu_v_a + (Cd_rear - Cd_tot)/1.3379613 + 3.9803717", + "best_score": 0.8432385530351132 +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/vortex_lamb_pysr.json b/src/SR_analysis/results/archive/raw/vortex_lamb_pysr.json new file mode 100644 index 0000000..ebc5c7d --- /dev/null +++ b/src/SR_analysis/results/archive/raw/vortex_lamb_pysr.json @@ -0,0 +1,8 @@ +{ + "scene": "vortex_lamb", + "mode": "pysr", + "similarity": 0.9489728204369429, + "action_range": 0.034138296000000005, + "n_steps": 150, + "threshold": "best" +} \ No newline at end of file diff --git a/src/SR_analysis/results/archive/raw/vortex_taylor_pysr.json b/src/SR_analysis/results/archive/raw/vortex_taylor_pysr.json new file mode 100644 index 0000000..f552132 --- /dev/null +++ b/src/SR_analysis/results/archive/raw/vortex_taylor_pysr.json @@ -0,0 +1,8 @@ +{ + "scene": "vortex_taylor", + "mode": "pysr", + "similarity": 0.9044944003424866, + "action_range": 0.034138296000000005, + "n_steps": 150, + "threshold": "best" +} \ No newline at end of file diff --git a/src/SR_analysis/results/formulas/illusion_0.75L_front.json b/src/SR_analysis/results/formulas/illusion_0.75L_front.json new file mode 100644 index 0000000..46bafb7 --- /dev/null +++ b/src/SR_analysis/results/formulas/illusion_0.75L_front.json @@ -0,0 +1,94 @@ +{ + "scene": "illusion_0.75L", + "scene_id": "illusion", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "Cd_err", + "Cl_err", + "dCd_err_dt", + "dCl_err_dt" + ], + "equations": [ + { + "complexity": 1, + "loss": 1.1611668, + "equation": "Cd_err", + "score": 0.0, + "sympy_format": "Cd_err", + "lambda_format": "PySRFunction(X=>Cd_err)" + }, + { + "complexity": 2, + "loss": 0.075797155, + "equation": "-1.2395082", + "score": 2.729119881666683, + "sympy_format": "-1.23950820000000", + "lambda_format": "PySRFunction(X=>-1.23950820000000)" + }, + { + "complexity": 4, + "loss": 0.057897076, + "equation": "Cl_err + -1.2561985", + "score": 0.13469693826111778, + "sympy_format": "Cl_err - 1.2561985", + "lambda_format": "PySRFunction(X=>Cl_err - 1.2561985)" + }, + { + "complexity": 6, + "loss": 0.03754608, + "equation": "-1.2428339 - (dCl_tot_dt / Cd_tot)", + "score": 0.21654895191031456, + "sympy_format": "-1.2428339 - dCl_tot_dt/Cd_tot", + "lambda_format": "PySRFunction(X=>-1.2428339 - dCl_tot_dt/Cd_tot)" + }, + { + "complexity": 8, + "loss": 0.027650228, + "equation": "-1.2424065 - ((dCl_tot_dt + Cl_tot) / Cd_tot)", + "score": 0.15296750237696044, + "sympy_format": "-1.2424065 - (Cl_tot + dCl_tot_dt)/Cd_tot", + "lambda_format": "PySRFunction(X=>-1.2424065 - (Cl_tot + dCl_tot_dt)/Cd_tot)" + }, + { + "complexity": 9, + "loss": 0.023363287, + "equation": "-1.2398432 - ((Cl_tot + dCl_tot_dt) * 0.16933453)", + "score": 0.1684681151879758, + "sympy_format": "-0.16933453*(Cl_tot + dCl_tot_dt) - 1.2398432", + "lambda_format": "PySRFunction(X=>-0.16933453*(Cl_tot + dCl_tot_dt) - 1.2398432)" + }, + { + "complexity": 11, + "loss": 0.021574462, + "equation": "((dCl_err_dt - (Cl_tot + dCl_tot_dt)) * 0.20579989) + -1.2389864", + "score": 0.039827779143953225, + "sympy_format": "(dCl_err_dt - (Cl_tot + dCl_tot_dt))*0.20579989 - 1.2389864", + "lambda_format": "PySRFunction(X=>(dCl_err_dt - (Cl_tot + dCl_tot_dt))*0.20579989 - 1.2389864)" + }, + { + "complexity": 13, + "loss": 0.020914195, + "equation": "((Cl_err / Cd_tot) + -1.2402159) - ((Cl_tot + dCl_tot_dt) * 0.16267194)", + "score": 0.015541092679475423, + "sympy_format": "-0.16267194*(Cl_tot + dCl_tot_dt) - 1.2402159 + Cl_err/Cd_tot", + "lambda_format": "PySRFunction(X=>-0.16267194*(Cl_tot + dCl_tot_dt) - 1.2402159 + Cl_err/Cd_tot)" + }, + { + "complexity": 15, + "loss": 0.020151647, + "equation": "(((Cl_err / Cd_rear) / Cd_tot) + -1.2398432) - ((Cl_tot + dCl_tot_dt) * 0.16933453)", + "score": 0.01857104650284237, + "sympy_format": "-0.16933453*(Cl_tot + dCl_tot_dt) - 1.2398432 + Cl_err/(Cd_rear*Cd_tot)", + "lambda_format": "PySRFunction(X=>-0.16933453*(Cl_tot + dCl_tot_dt) - 1.2398432 + Cl_err/(Cd_rear*Cd_tot))" + } + ], + "best_sympy": "-0.16933453*(Cl_tot + dCl_tot_dt) - 1.2398432", + "best_score": 0.6917657052732269 +} \ No newline at end of file diff --git a/src/SR_analysis/results/formulas/illusion_0.75L_front_target.json b/src/SR_analysis/results/formulas/illusion_0.75L_front_target.json new file mode 100644 index 0000000..021ce74 --- /dev/null +++ b/src/SR_analysis/results/formulas/illusion_0.75L_front_target.json @@ -0,0 +1,111 @@ +{ + "scene": "illusion_0.75L", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "Cd_err", + "Cl_err", + "dCd_err_dt", + "dCl_err_dt", + "target_Cd", + "target_Cl" + ], + "best_sympy": "-(dCl_tot_dt + target_Cl)/5.8401647 - 1.2372783", + "best_score": 0.7232722304555419, + "equations": [ + { + "complexity": 1, + "loss": 1.6359515, + "equation": "Cl_err", + "score": 0.0, + "sympy_format": "Cl_err", + "lambda_format": "PySRFunction(X=>Cl_err)" + }, + { + "complexity": 2, + "loss": 0.075797155, + "equation": "-1.2395153", + "score": 3.071919112284068, + "sympy_format": "-1.23951530000000", + "lambda_format": "PySRFunction(X=>-1.23951530000000)" + }, + { + "complexity": 4, + "loss": 0.057897076, + "equation": "Cl_err - 1.2562019", + "score": 0.13469693826111778, + "sympy_format": "Cl_err - 1*1.2562019", + "lambda_format": "PySRFunction(X=>Cl_err - 1*1.2562019)" + }, + { + "complexity": 6, + "loss": 0.037532225, + "equation": "-1.2431892 - (dCl_tot_dt / target_Cd)", + "score": 0.2167334925729904, + "sympy_format": "-dCl_tot_dt/target_Cd - 1.2431892", + "lambda_format": "PySRFunction(X=>-dCl_tot_dt/target_Cd - 1.2431892)" + }, + { + "complexity": 8, + "loss": 0.026256593, + "equation": "-1.2326334 - ((Cl_tot + dCl_tot_dt) / target_Cd)", + "score": 0.1786413889438378, + "sympy_format": "-1.2326334 - (Cl_tot + dCl_tot_dt)/target_Cd", + "lambda_format": "PySRFunction(X=>-1.2326334 - (Cl_tot + dCl_tot_dt)/target_Cd)" + }, + { + "complexity": 9, + "loss": 0.020975176, + "equation": "-1.2372783 - ((target_Cl + dCl_tot_dt) / 5.8401647)", + "score": 0.22457747614686055, + "sympy_format": "-(dCl_tot_dt + target_Cl)/5.8401647 - 1.2372783", + "lambda_format": "PySRFunction(X=>-(dCl_tot_dt + target_Cl)/5.8401647 - 1.2372783)" + }, + { + "complexity": 10, + "loss": 0.019975036, + "equation": "-1.2351149 - ((dCl_tot_dt + target_Cl) / (target_Cd + Cd_rear))", + "score": 0.0488563493561329, + "sympy_format": "-1.2351149 - (dCl_tot_dt + target_Cl)/(Cd_rear + target_Cd)", + "lambda_format": "PySRFunction(X=>-1.2351149 - (dCl_tot_dt + target_Cl)/(Cd_rear + target_Cd))" + }, + { + "complexity": 11, + "loss": 0.018287793, + "equation": "-1.2386359 - ((target_Cl + (dCl_tot_dt + dCd_err_dt)) / 5.8050957)", + "score": 0.08824950581277256, + "sympy_format": "-(dCd_err_dt + dCl_tot_dt + target_Cl)/5.8050957 - 1.2386359", + "lambda_format": "PySRFunction(X=>-(dCd_err_dt + dCl_tot_dt + target_Cl)/5.8050957 - 1.2386359)" + }, + { + "complexity": 12, + "loss": 0.016175408, + "equation": "-1.2290057 - ((((dCd_err_dt + target_Cl) + dCl_tot_dt) / target_Cd) / Cd_rear)", + "score": 0.12274172391063341, + "sympy_format": "-1.2290057 - (dCd_err_dt + dCl_tot_dt + target_Cl)/(Cd_rear*target_Cd)", + "lambda_format": "PySRFunction(X=>-1.2290057 - (dCd_err_dt + dCl_tot_dt + target_Cl)/(Cd_rear*target_Cd))" + }, + { + "complexity": 13, + "loss": 0.01565276, + "equation": "-1.2277503 - ((((dCl_tot_dt + dCd_err_dt) + target_Cl) / 3.854052) / Cd_rear)", + "score": 0.03284480491569963, + "sympy_format": "-1.2277503 - (dCd_err_dt + dCl_tot_dt + target_Cl)/(3.854052*Cd_rear)", + "lambda_format": "PySRFunction(X=>-1.2277503 - (dCd_err_dt + dCl_tot_dt + target_Cl)/(3.854052*Cd_rear))" + }, + { + "complexity": 14, + "loss": 0.014618657, + "equation": "-1.1933857 - (((dCl_tot_dt + dCd_err_dt) + target_Cl) / ((target_Cd - target_Cl) * Cd_rear))", + "score": 0.0683486696273396, + "sympy_format": "-1.1933857 - (dCd_err_dt + dCl_tot_dt + target_Cl)/(Cd_rear*(target_Cd - target_Cl))", + "lambda_format": "PySRFunction(X=>-1.1933857 - (dCd_err_dt + dCl_tot_dt + target_Cl)/(Cd_rear*(target_Cd - target_Cl)))" + } + ] +} \ No newline at end of file diff --git a/src/SR_analysis/results/formulas/illusion_0.75L_top.json b/src/SR_analysis/results/formulas/illusion_0.75L_top.json new file mode 100644 index 0000000..d2a4fb9 --- /dev/null +++ b/src/SR_analysis/results/formulas/illusion_0.75L_top.json @@ -0,0 +1,119 @@ +{ + "scene": "illusion_0.75L", + "scene_id": "illusion", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "Cd_err", + "Cl_err", + "dCd_err_dt", + "dCl_err_dt" + ], + "equations": [ + { + "complexity": 1, + "loss": 0.45252356, + "equation": "Cd_rear", + "score": 0.0, + "sympy_format": "Cd_rear", + "lambda_format": "PySRFunction(X=>Cd_rear)" + }, + { + "complexity": 2, + "loss": 0.1519909, + "equation": "1.8688796", + "score": 1.0910191774498823, + "sympy_format": "1.86887960000000", + "lambda_format": "PySRFunction(X=>1.86887960000000)" + }, + { + "complexity": 4, + "loss": 0.1296351, + "equation": "Cl_err + 1.8521569", + "score": 0.07954853502512871, + "sympy_format": "Cl_err + 1.8521569", + "lambda_format": "PySRFunction(X=>Cl_err + 1.8521569)" + }, + { + "complexity": 6, + "loss": 0.075855345, + "equation": "(dCl_tot_dt - Cd_tot) * -0.45136496", + "score": 0.267947704620995, + "sympy_format": "(-Cd_tot + dCl_tot_dt)*(-0.45136496)", + "lambda_format": "PySRFunction(X=>(-Cd_tot + dCl_tot_dt)*(-0.45136496))" + }, + { + "complexity": 7, + "loss": 0.06326852, + "equation": "(u_a * -0.016224489) + 1.88584", + "score": 0.18144028026845632, + "sympy_format": "1.88584 + u_a*(-0.016224489)", + "lambda_format": "PySRFunction(X=>1.88584 + u_a*(-0.016224489))" + }, + { + "complexity": 8, + "loss": 0.056762762, + "equation": "((dCl_tot_dt - Cd_tot) - dCl_err_dt) * -0.44981483", + "score": 0.10850737893550855, + "sympy_format": "(-Cd_tot - dCl_err_dt + dCl_tot_dt)*(-0.44981483)", + "lambda_format": "PySRFunction(X=>(-Cd_tot - dCl_err_dt + dCl_tot_dt)*(-0.44981483))" + }, + { + "complexity": 9, + "loss": 0.039528407, + "equation": "(Cl_err - (u_a * 0.016350273)) + 1.8692697", + "score": 0.36186093414770126, + "sympy_format": "Cl_err - 0.016350273*u_a + 1.8692697", + "lambda_format": "PySRFunction(X=>Cl_err - 0.016350273*u_a + 1.8692697)" + }, + { + "complexity": 10, + "loss": 0.038722, + "equation": "Cl_err + square((u_a * 0.00608778) - 1.3626369)", + "score": 0.020611664022151262, + "sympy_format": "Cl_err + (u_a*0.00608778 - 1*1.3626369)**2", + "lambda_format": "PySRFunction(X=>Cl_err + (u_a*0.00608778 - 1*1.3626369)**2)" + }, + { + "complexity": 11, + "loss": 0.03458842, + "equation": "Cd_err + (Cl_err + ((u_a * -0.014238624) - -2.0921295))", + "score": 0.11288897000276664, + "sympy_format": "Cd_err + Cl_err + u_a*(-0.014238624) - 1*(-2.0921295)", + "lambda_format": "PySRFunction(X=>Cd_err + Cl_err + u_a*(-0.014238624) - 1*(-2.0921295))" + }, + { + "complexity": 12, + "loss": 0.031432744, + "equation": "(((u_a * -0.015429008) - -1.9807403) - square(Cd_err)) + Cl_err", + "score": 0.09566879184310657, + "sympy_format": "-Cd_err**2 + Cl_err + u_a*(-0.015429008) - 1*(-1.9807403)", + "lambda_format": "PySRFunction(X=>-Cd_err**2 + Cl_err + u_a*(-0.015429008) - 1*(-1.9807403))" + }, + { + "complexity": 13, + "loss": 0.030870467, + "equation": "(Cl_err + square((u_a * 0.0055642533) - 1.4036555)) - square(Cd_err)", + "score": 0.01805018576551437, + "sympy_format": "-Cd_err**2 + Cl_err + (u_a*0.0055642533 - 1*1.4036555)**2", + "lambda_format": "PySRFunction(X=>-Cd_err**2 + Cl_err + (u_a*0.0055642533 - 1*1.4036555)**2)" + }, + { + "complexity": 14, + "loss": 0.021888444, + "equation": "((Cl_err + (Cd_err * 0.5409948)) + (u_a * -0.015207872)) + 1.9898309", + "score": 0.3438411400334662, + "sympy_format": "Cd_err*0.5409948 + Cl_err + u_a*(-0.015207872) + 1.9898309", + "lambda_format": "PySRFunction(X=>Cd_err*0.5409948 + Cl_err + u_a*(-0.015207872) + 1.9898309)" + } + ], + "best_sympy": "Cd_err*0.5409948 + Cl_err + u_a*(-0.015207872) + 1.9898309", + "best_score": 0.8559884564869915 +} \ No newline at end of file diff --git a/src/SR_analysis/results/formulas/illusion_0.75L_top_target.json b/src/SR_analysis/results/formulas/illusion_0.75L_top_target.json new file mode 100644 index 0000000..3125a3d --- /dev/null +++ b/src/SR_analysis/results/formulas/illusion_0.75L_top_target.json @@ -0,0 +1,112 @@ +{ + "scene": "illusion_0.75L", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "Cd_err", + "Cl_err", + "dCd_err_dt", + "dCl_err_dt", + "target_Cd", + "target_Cl" + ], + "best_sympy": "Cd_tot*0.45037818 + Cl_err - 0.016348997*u_a", + "best_score": 0.8375927169907114, + "equations": [ + { + "complexity": 1, + "loss": 0.45252356, + "equation": "Cd_rear", + "score": 0.0, + "sympy_format": "Cd_rear", + "lambda_format": "PySRFunction(X=>Cd_rear)" + }, + { + "complexity": 2, + "loss": 0.1519909, + "equation": "1.8688794", + "score": 1.0910191774498823, + "sympy_format": "1.86887940000000", + "lambda_format": "PySRFunction(X=>1.86887940000000)" + }, + { + "complexity": 4, + "loss": 0.1296351, + "equation": "Cl_err + 1.8521743", + "score": 0.07954853502512871, + "sympy_format": "Cl_err + 1.8521743", + "lambda_format": "PySRFunction(X=>Cl_err + 1.8521743)" + }, + { + "complexity": 6, + "loss": 0.074802674, + "equation": "(8.007482 - dCl_tot_dt) / target_Cd", + "score": 0.2749349737891081, + "sympy_format": "(8.007482 - dCl_tot_dt)/target_Cd", + "lambda_format": "PySRFunction(X=>(8.007482 - dCl_tot_dt)/target_Cd)" + }, + { + "complexity": 7, + "loss": 0.06326852, + "equation": "1.8858196 - (u_a * 0.01622415)", + "score": 0.16746574193223032, + "sympy_format": "1.8858196 - 0.01622415*u_a", + "lambda_format": "PySRFunction(X=>1.8858196 - 0.01622415*u_a)" + }, + { + "complexity": 8, + "loss": 0.04728117, + "equation": "((Cl_tot + 7.9624605) - dCl_tot_dt) / target_Cd", + "score": 0.29127577208778055, + "sympy_format": "(Cl_tot - dCl_tot_dt + 7.9624605)/target_Cd", + "lambda_format": "PySRFunction(X=>(Cl_tot - dCl_tot_dt + 7.9624605)/target_Cd)" + }, + { + "complexity": 9, + "loss": 0.039528407, + "equation": "(Cl_err + 1.8692691) - (u_a * 0.016348997)", + "score": 0.17909254099542934, + "sympy_format": "Cl_err - 0.016348997*u_a + 1.8692691", + "lambda_format": "PySRFunction(X=>Cl_err - 0.016348997*u_a + 1.8692691)" + }, + { + "complexity": 10, + "loss": 0.037286226, + "equation": "(((target_Cd - dCl_tot_dt) * 1.8496238) + target_Cl) / target_Cd", + "score": 0.05839559567798887, + "sympy_format": "(target_Cl + (-dCl_tot_dt + target_Cd)*1.8496238)/target_Cd", + "lambda_format": "PySRFunction(X=>(target_Cl + (-dCl_tot_dt + target_Cd)*1.8496238)/target_Cd)" + }, + { + "complexity": 11, + "loss": 0.024684431, + "equation": "Cl_err + ((Cd_tot * 0.45037818) - (u_a * 0.016348997))", + "score": 0.41245126130146226, + "sympy_format": "Cd_tot*0.45037818 + Cl_err - 0.016348997*u_a", + "lambda_format": "PySRFunction(X=>Cd_tot*0.45037818 + Cl_err - 0.016348997*u_a)" + }, + { + "complexity": 13, + "loss": 0.018228356, + "equation": "(((dCl_err_dt + target_Cd) - dCl_tot_dt) * 0.4332428) + (du_a_dt / -94.089226)", + "score": 0.15159715870297544, + "sympy_format": "du_a_dt/(-94.089226) + (dCl_err_dt - dCl_tot_dt + target_Cd)*0.4332428", + "lambda_format": "PySRFunction(X=>du_a_dt/(-94.089226) + (dCl_err_dt - dCl_tot_dt + target_Cd)*0.4332428)" + }, + { + "complexity": 15, + "loss": 0.017791519, + "equation": "(((dCl_err_dt + target_Cd) - dCl_tot_dt) * 0.432934) + ((du_a_dt + dCl_tot_dt) / -91.89213)", + "score": 0.012128260261647603, + "sympy_format": "(dCl_tot_dt + du_a_dt)/(-91.89213) + (dCl_err_dt - dCl_tot_dt + target_Cd)*0.432934", + "lambda_format": "PySRFunction(X=>(dCl_tot_dt + du_a_dt)/(-91.89213) + (dCl_err_dt - dCl_tot_dt + target_Cd)*0.432934)" + } + ] +} \ No newline at end of file diff --git a/src/SR_analysis/results/formulas/illusion_1.5L_front.json b/src/SR_analysis/results/formulas/illusion_1.5L_front.json new file mode 100644 index 0000000..13c98e1 --- /dev/null +++ b/src/SR_analysis/results/formulas/illusion_1.5L_front.json @@ -0,0 +1,45 @@ +{ + "scene": "illusion_1.5L", + "channel": "front", + "feature_names": [ + "Cd_tot", + "Cd_rear", + "Cl_tot", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT", + "target_Cd", + "target_Cl" + ], + "equations": [ + { + "complexity": 1, + "loss": 9.924878, + "equation": "Cl_tot", + "score": 0.0, + "sympy_format": "Cl_tot", + "lambda_format": "PySRFunction(X=>Cl_tot)" + }, + { + "complexity": 2, + "loss": 0.0019758705, + "equation": "0.0019602603", + "score": 8.521790752547583, + "sympy_format": "0.00196026030000000", + "lambda_format": "PySRFunction(X=>0.00196026030000000)" + }, + { + "complexity": 4, + "loss": 2.7072198999999996e-18, + "equation": "aF_lag1 * 0.01", + "score": 17.11193160724022, + "sympy_format": "aF_lag1*0.01", + "lambda_format": "PySRFunction(X=>aF_lag1*0.01)" + } + ], + "best_sympy": "aF_lag1*0.01", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/results/formulas/illusion_1.5L_top.json b/src/SR_analysis/results/formulas/illusion_1.5L_top.json new file mode 100644 index 0000000..4b27fcd --- /dev/null +++ b/src/SR_analysis/results/formulas/illusion_1.5L_top.json @@ -0,0 +1,55 @@ +{ + "scene": "illusion_1.5L", + "channel": "top", + "feature_names": [ + "bias", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT", + "target_Cd", + "target_Cl" + ], + "equations": [ + { + "complexity": 1, + "loss": 1.0037948, + "equation": "bias", + "score": 0.0, + "sympy_format": "bias", + "lambda_format": "PySRFunction(X=>bias)" + }, + { + "complexity": 2, + "loss": 0.0007045742, + "equation": "-0.0015421732", + "score": 7.261704527158024, + "sympy_format": "-0.00154217320000000", + "lambda_format": "PySRFunction(X=>-0.00154217320000000)" + }, + { + "complexity": 4, + "loss": 2.5116741e-13, + "equation": "aT_lag1 / 99.998116", + "score": 10.877369898373423, + "sympy_format": "aT_lag1/99.998116", + "lambda_format": "PySRFunction(X=>aT_lag1/99.998116)" + }, + { + "complexity": 6, + "loss": 1.5754642e-18, + "equation": "(aT_lag1 + aT_lag1) * 0.005", + "score": 5.989662504443251, + "sympy_format": "(aT_lag1 + aT_lag1)*0.005", + "lambda_format": "PySRFunction(X=>(aT_lag1 + aT_lag1)*0.005)" + } + ], + "best_sympy": "(aT_lag1 + aT_lag1)*0.005", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/results/formulas/illusion_1L_front.json b/src/SR_analysis/results/formulas/illusion_1L_front.json new file mode 100644 index 0000000..811f7d4 --- /dev/null +++ b/src/SR_analysis/results/formulas/illusion_1L_front.json @@ -0,0 +1,110 @@ +{ + "scene": "illusion_1L", + "scene_id": "illusion", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "Cd_err", + "Cl_err", + "dCd_err_dt", + "dCl_err_dt" + ], + "equations": [ + { + "complexity": 1, + "loss": 0.48229608, + "equation": "dCl_err_dt", + "score": 0.0, + "sympy_format": "dCl_err_dt", + "lambda_format": "PySRFunction(X=>dCl_err_dt)" + }, + { + "complexity": 2, + "loss": 0.15649326, + "equation": "0.33449104", + "score": 1.1255452573604185, + "sympy_format": "0.334491040000000", + "lambda_format": "PySRFunction(X=>0.334491040000000)" + }, + { + "complexity": 4, + "loss": 0.14671756, + "equation": "1.1508505 / Cd_rear", + "score": 0.03225178195326072, + "sympy_format": "1.1508505/Cd_rear", + "lambda_format": "PySRFunction(X=>1.1508505/Cd_rear)" + }, + { + "complexity": 5, + "loss": 0.14011887, + "equation": "(Cd_tot - Cd_rear) / Cd_tot", + "score": 0.0460182442729821, + "sympy_format": "(-Cd_rear + Cd_tot)/Cd_tot", + "lambda_format": "PySRFunction(X=>(-Cd_rear + Cd_tot)/Cd_tot)" + }, + { + "complexity": 6, + "loss": 0.0863823, + "equation": "(Cl_tot + 1.8902351) / Cd_tot", + "score": 0.4837083400686436, + "sympy_format": "(Cl_tot + 1.8902351)/Cd_tot", + "lambda_format": "PySRFunction(X=>(Cl_tot + 1.8902351)/Cd_tot)" + }, + { + "complexity": 7, + "loss": 0.047962923, + "equation": "(u_a * 0.015167036) + 0.31766427", + "score": 0.5883545187876664, + "sympy_format": "u_a*0.015167036 + 0.31766427", + "lambda_format": "PySRFunction(X=>u_a*0.015167036 + 0.31766427)" + }, + { + "complexity": 8, + "loss": 0.043905187, + "equation": "((Cl_tot - dCl_tot_dt) + 1.8545729) / Cd_tot", + "score": 0.08839580692544886, + "sympy_format": "(Cl_tot - dCl_tot_dt + 1.8545729)/Cd_tot", + "lambda_format": "PySRFunction(X=>(Cl_tot - dCl_tot_dt + 1.8545729)/Cd_tot)" + }, + { + "complexity": 9, + "loss": 0.011135678, + "equation": "((u_a + du_a_dt) + 26.506319) * 0.012334255", + "score": 1.3718782800547544, + "sympy_format": "(du_a_dt + u_a + 26.506319)*0.012334255", + "lambda_format": "PySRFunction(X=>(du_a_dt + u_a + 26.506319)*0.012334255)" + }, + { + "complexity": 11, + "loss": 0.010412063, + "equation": "(u_a + ((26.81757 - dCl_tot_dt) + du_a_dt)) * 0.01217924", + "score": 0.03359457505969681, + "sympy_format": "(-dCl_tot_dt + du_a_dt + u_a + 26.81757)*0.01217924", + "lambda_format": "PySRFunction(X=>(-dCl_tot_dt + du_a_dt + u_a + 26.81757)*0.01217924)" + }, + { + "complexity": 12, + "loss": 0.009215188, + "equation": "(du_a_dt * 0.010480981) + ((u_a * 0.0138015505) + 0.32438698)", + "score": 0.12211204539179134, + "sympy_format": "du_a_dt*0.010480981 + u_a*0.0138015505 + 0.32438698", + "lambda_format": "PySRFunction(X=>du_a_dt*0.010480981 + u_a*0.0138015505 + 0.32438698)" + }, + { + "complexity": 14, + "loss": 0.008032124, + "equation": "((du_a_dt * 0.009704931) + ((u_a + Cl_tot) * 0.013764588)) + 0.32533023", + "score": 0.06870199567786181, + "sympy_format": "du_a_dt*0.009704931 + (Cl_tot + u_a)*0.013764588 + 0.32533023", + "lambda_format": "PySRFunction(X=>du_a_dt*0.009704931 + (Cl_tot + u_a)*0.013764588 + 0.32533023)" + } + ], + "best_sympy": "(du_a_dt + u_a + 26.506319)*0.012334255", + "best_score": 0.9288424393884038 +} \ No newline at end of file diff --git a/src/SR_analysis/results/formulas/illusion_1L_front_target.json b/src/SR_analysis/results/formulas/illusion_1L_front_target.json new file mode 100644 index 0000000..93f94d8 --- /dev/null +++ b/src/SR_analysis/results/formulas/illusion_1L_front_target.json @@ -0,0 +1,127 @@ +{ + "scene": "illusion_1L", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "Cd_err", + "Cl_err", + "dCd_err_dt", + "dCl_err_dt", + "target_Cd", + "target_Cl" + ], + "best_sympy": "(du_a_dt + u_a + 26.506351)*0.012334249", + "best_score": 0.9288424393879982, + "equations": [ + { + "complexity": 1, + "loss": 0.29883155, + "equation": "dCd_err_dt", + "score": 0.0, + "sympy_format": "dCd_err_dt", + "lambda_format": "PySRFunction(X=>dCd_err_dt)" + }, + { + "complexity": 2, + "loss": 0.15649326, + "equation": "0.33446583", + "score": 0.6468670947471492, + "sympy_format": "0.334465830000000", + "lambda_format": "PySRFunction(X=>0.334465830000000)" + }, + { + "complexity": 4, + "loss": 0.14671756, + "equation": "1.1508491 / Cd_rear", + "score": 0.03225178195326072, + "sympy_format": "1.1508491/Cd_rear", + "lambda_format": "PySRFunction(X=>1.1508491/Cd_rear)" + }, + { + "complexity": 5, + "loss": 0.14011887, + "equation": "(Cd_tot - Cd_rear) / Cd_tot", + "score": 0.0460182442729821, + "sympy_format": "(-Cd_rear + Cd_tot)/Cd_tot", + "lambda_format": "PySRFunction(X=>(-Cd_rear + Cd_tot)/Cd_tot)" + }, + { + "complexity": 6, + "loss": 0.08638231, + "equation": "(Cl_tot + 1.8901986) / Cd_tot", + "score": 0.48370822430419386, + "sympy_format": "(Cl_tot + 1.8901986)/Cd_tot", + "lambda_format": "PySRFunction(X=>(Cl_tot + 1.8901986)/Cd_tot)" + }, + { + "complexity": 7, + "loss": 0.047962923, + "equation": "(u_a * 0.015167245) + 0.3176641", + "score": 0.5883546345521161, + "sympy_format": "u_a*0.015167245 + 0.3176641", + "lambda_format": "PySRFunction(X=>u_a*0.015167245 + 0.3176641)" + }, + { + "complexity": 8, + "loss": 0.04292227, + "equation": "((Cl_tot - dCl_tot_dt) / target_Cd) + 0.34500188", + "score": 0.11103746946441734, + "sympy_format": "0.34500188 + (Cl_tot - dCl_tot_dt)/target_Cd", + "lambda_format": "PySRFunction(X=>0.34500188 + (Cl_tot - dCl_tot_dt)/target_Cd)" + }, + { + "complexity": 9, + "loss": 0.011135677, + "equation": "(u_a + (du_a_dt + 26.506351)) * 0.012334249", + "score": 1.349236707317237, + "sympy_format": "(du_a_dt + u_a + 26.506351)*0.012334249", + "lambda_format": "PySRFunction(X=>(du_a_dt + u_a + 26.506351)*0.012334249)" + }, + { + "complexity": 11, + "loss": 0.010412063, + "equation": "((du_a_dt - dCl_tot_dt) + (u_a + 26.817722)) * 0.012179124", + "score": 0.03359453015897125, + "sympy_format": "(-dCl_tot_dt + du_a_dt + u_a + 26.817722)*0.012179124", + "lambda_format": "PySRFunction(X=>(-dCl_tot_dt + du_a_dt + u_a + 26.817722)*0.012179124)" + }, + { + "complexity": 12, + "loss": 0.009215188, + "equation": "(du_a_dt * 0.010480981) + ((u_a * 0.0138015505) + 0.32438707)", + "score": 0.12211204539179134, + "sympy_format": "du_a_dt*0.010480981 + u_a*0.0138015505 + 0.32438707", + "lambda_format": "PySRFunction(X=>du_a_dt*0.010480981 + u_a*0.0138015505 + 0.32438707)" + }, + { + "complexity": 13, + "loss": 0.00894252, + "equation": "(((du_a_dt + u_a) + 22.995256) - (du_a_dt / Cd_rear)) * 0.014093986", + "score": 0.030035563773362823, + "sympy_format": "(du_a_dt + u_a + 22.995256 - du_a_dt/Cd_rear)*0.014093986", + "lambda_format": "PySRFunction(X=>(du_a_dt + u_a + 22.995256 - du_a_dt/Cd_rear)*0.014093986)" + }, + { + "complexity": 14, + "loss": 0.008032124, + "equation": "((u_a + Cl_tot) + ((du_a_dt / 1.4181519) + 23.637499)) * 0.013763895", + "score": 0.10736842758236086, + "sympy_format": "(Cl_tot + du_a_dt/1.4181519 + u_a + 23.637499)*0.013763895", + "lambda_format": "PySRFunction(X=>(Cl_tot + du_a_dt/1.4181519 + u_a + 23.637499)*0.013763895)" + }, + { + "complexity": 15, + "loss": 0.0077406703, + "equation": "(du_a_dt + (((Cl_tot + 23.718166) - (du_a_dt / Cd_rear)) + u_a)) * 0.013728722", + "score": 0.0369607151581401, + "sympy_format": "(Cl_tot + du_a_dt + u_a + 23.718166 - du_a_dt/Cd_rear)*0.013728722", + "lambda_format": "PySRFunction(X=>(Cl_tot + du_a_dt + u_a + 23.718166 - du_a_dt/Cd_rear)*0.013728722)" + } + ] +} \ No newline at end of file diff --git a/src/SR_analysis/results/formulas/illusion_1L_top.json b/src/SR_analysis/results/formulas/illusion_1L_top.json new file mode 100644 index 0000000..505177d --- /dev/null +++ b/src/SR_analysis/results/formulas/illusion_1L_top.json @@ -0,0 +1,135 @@ +{ + "scene": "illusion_1L", + "scene_id": "illusion", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "Cd_err", + "Cl_err", + "dCd_err_dt", + "dCl_err_dt" + ], + "equations": [ + { + "complexity": 1, + "loss": 0.11277538, + "equation": "bias", + "score": 0.0, + "sympy_format": "bias", + "lambda_format": "PySRFunction(X=>bias)" + }, + { + "complexity": 2, + "loss": 0.06636527, + "equation": "0.7845695", + "score": 0.5302241753504614, + "sympy_format": "0.784569500000000", + "lambda_format": "PySRFunction(X=>0.784569500000000)" + }, + { + "complexity": 3, + "loss": 0.066365264, + "equation": "square(0.88574606)", + "score": 9.040873758650598e-08, + "sympy_format": "0.784546082805524", + "lambda_format": "PySRFunction(X=>0.784546082805524)" + }, + { + "complexity": 4, + "loss": 0.065482475, + "equation": "2.5905075 / Cd_rear", + "score": 0.013391237481234886, + "sympy_format": "2.5905075/Cd_rear", + "lambda_format": "PySRFunction(X=>2.5905075/Cd_rear)" + }, + { + "complexity": 5, + "loss": 0.05560235, + "equation": "Cd_tot / (Cd_rear + Cd_rear)", + "score": 0.16355708303907351, + "sympy_format": "Cd_tot/(Cd_rear + Cd_rear)", + "lambda_format": "PySRFunction(X=>Cd_tot/(Cd_rear + Cd_rear))" + }, + { + "complexity": 6, + "loss": 0.037583124, + "equation": "0.7779934 - (dCl_tot_dt / Cd_tot)", + "score": 0.3916703466960199, + "sympy_format": "0.7779934 - dCl_tot_dt/Cd_tot", + "lambda_format": "PySRFunction(X=>0.7779934 - dCl_tot_dt/Cd_tot)" + }, + { + "complexity": 7, + "loss": 0.035889544, + "equation": "(dCl_tot_dt * -0.14946744) + 0.7795025", + "score": 0.04610912026457068, + "sympy_format": "0.7795025 + dCl_tot_dt*(-0.14946744)", + "lambda_format": "PySRFunction(X=>0.7795025 + dCl_tot_dt*(-0.14946744))" + }, + { + "complexity": 8, + "loss": 0.029347742, + "equation": "0.7866203 - (dCl_tot_dt / (Cd_tot - Cl_tot))", + "score": 0.2012303898357161, + "sympy_format": "0.7866203 - dCl_tot_dt/(Cd_tot - Cl_tot)", + "lambda_format": "PySRFunction(X=>0.7866203 - dCl_tot_dt/(Cd_tot - Cl_tot))" + }, + { + "complexity": 10, + "loss": 0.02741706, + "equation": "(dCl_tot_dt / (Cl_tot + (Cl_err - Cd_tot))) + 0.7815183", + "score": 0.03402508118753478, + "sympy_format": "dCl_tot_dt/(-Cd_tot + Cl_err + Cl_tot) + 0.7815183", + "lambda_format": "PySRFunction(X=>dCl_tot_dt/(-Cd_tot + Cl_err + Cl_tot) + 0.7815183)" + }, + { + "complexity": 11, + "loss": 0.025678542, + "equation": "0.77942413 - (((dCl_tot_dt * Cl_tot) + dCl_tot_dt) / square(Cd_rear))", + "score": 0.06550974586999601, + "sympy_format": "0.77942413 - (Cl_tot*dCl_tot_dt + dCl_tot_dt)/(Cd_rear**2)", + "lambda_format": "PySRFunction(X=>0.77942413 - (Cl_tot*dCl_tot_dt + dCl_tot_dt)/(Cd_rear**2))" + }, + { + "complexity": 12, + "loss": 0.020175973, + "equation": "0.7962736 - ((Cl_tot + bias) * (dCl_tot_dt / (Cd_rear + Cd_rear)))", + "score": 0.2411632605109884, + "sympy_format": "0.7962736 - dCl_tot_dt*(Cl_tot + bias)/(Cd_rear + Cd_rear)", + "lambda_format": "PySRFunction(X=>0.7962736 - dCl_tot_dt*(Cl_tot + bias)/(Cd_rear + Cd_rear))" + }, + { + "complexity": 13, + "loss": 0.018688664, + "equation": "0.8023191 - ((dCl_tot_dt * (Cl_tot + 1.3881962)) / (Cd_rear + Cd_tot))", + "score": 0.07657530416150655, + "sympy_format": "0.8023191 - dCl_tot_dt*(Cl_tot + 1.3881962)/(Cd_rear + Cd_tot)", + "lambda_format": "PySRFunction(X=>0.8023191 - dCl_tot_dt*(Cl_tot + 1.3881962)/(Cd_rear + Cd_tot))" + }, + { + "complexity": 14, + "loss": 0.016381161, + "equation": "0.7874223 - (dCl_tot_dt * (((Cl_tot + bias) + Cl_err) / (Cd_rear + Cd_rear)))", + "score": 0.13178518180253324, + "sympy_format": "0.7874223 - dCl_tot_dt*(Cl_err + Cl_tot + bias)/(Cd_rear + Cd_rear)", + "lambda_format": "PySRFunction(X=>0.7874223 - dCl_tot_dt*(Cl_err + Cl_tot + bias)/(Cd_rear + Cd_rear))" + }, + { + "complexity": 15, + "loss": 0.015024375, + "equation": "0.78738326 - ((dCl_tot_dt * ((Cl_err + 1.4685087) + Cl_tot)) / (Cd_rear + Cd_tot))", + "score": 0.08645807281568882, + "sympy_format": "0.78738326 - dCl_tot_dt*(Cl_err + Cl_tot + 1.4685087)/(Cd_rear + Cd_tot)", + "lambda_format": "PySRFunction(X=>0.78738326 - dCl_tot_dt*(Cl_err + Cl_tot + 1.4685087)/(Cd_rear + Cd_tot))" + } + ], + "best_sympy": "0.7962736 - dCl_tot_dt*(Cl_tot + bias)/(Cd_rear + Cd_rear)", + "best_score": 0.6959860204249317 +} \ No newline at end of file diff --git a/src/SR_analysis/results/formulas/illusion_1L_top_target.json b/src/SR_analysis/results/formulas/illusion_1L_top_target.json new file mode 100644 index 0000000..36e7bd8 --- /dev/null +++ b/src/SR_analysis/results/formulas/illusion_1L_top_target.json @@ -0,0 +1,136 @@ +{ + "scene": "illusion_1L", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "Cd_err", + "Cl_err", + "dCd_err_dt", + "dCl_err_dt", + "target_Cd", + "target_Cl" + ], + "best_sympy": "dCl_tot_dt*(-0.13315448)*(Cl_tot + bias) + 0.80922115", + "best_score": 0.6947513140376443, + "equations": [ + { + "complexity": 1, + "loss": 0.11277538, + "equation": "bias", + "score": 0.0, + "sympy_format": "bias", + "lambda_format": "PySRFunction(X=>bias)" + }, + { + "complexity": 2, + "loss": 0.066365264, + "equation": "0.7845461", + "score": 0.530224265759199, + "sympy_format": "0.784546100000000", + "lambda_format": "PySRFunction(X=>0.784546100000000)" + }, + { + "complexity": 4, + "loss": 0.0651199, + "equation": "target_Cd + -4.860893", + "score": 0.00947180050206764, + "sympy_format": "target_Cd - 4.860893", + "lambda_format": "PySRFunction(X=>target_Cd - 4.860893)" + }, + { + "complexity": 5, + "loss": 0.05987383, + "equation": "Cd_rear / (Cd_tot + dCl_tot_dt)", + "score": 0.08399067123622497, + "sympy_format": "Cd_rear/(Cd_tot + dCl_tot_dt)", + "lambda_format": "PySRFunction(X=>Cd_rear/(Cd_tot + dCl_tot_dt))" + }, + { + "complexity": 6, + "loss": 0.03714929, + "equation": "0.77852744 - (dCl_tot_dt / target_Cd)", + "score": 0.47729485555849455, + "sympy_format": "0.77852744 - dCl_tot_dt/target_Cd", + "lambda_format": "PySRFunction(X=>0.77852744 - dCl_tot_dt/target_Cd)" + }, + { + "complexity": 7, + "loss": 0.03588954, + "equation": "(dCl_tot_dt * -0.14946803) + 0.77950186", + "score": 0.034498771135124374, + "sympy_format": "0.77950186 + dCl_tot_dt*(-0.14946803)", + "lambda_format": "PySRFunction(X=>0.77950186 + dCl_tot_dt*(-0.14946803))" + }, + { + "complexity": 8, + "loss": 0.028979579, + "equation": "(dCl_tot_dt / (Cl_tot - target_Cd)) + 0.78621364", + "score": 0.21385447860989099, + "sympy_format": "dCl_tot_dt/(Cl_tot - target_Cd) + 0.78621364", + "lambda_format": "PySRFunction(X=>dCl_tot_dt/(Cl_tot - target_Cd) + 0.78621364)" + }, + { + "complexity": 9, + "loss": 0.028761907, + "equation": "(dCl_tot_dt / (Cl_tot + -5.7742095)) + 0.7856848", + "score": 0.007539571500876667, + "sympy_format": "dCl_tot_dt/(Cl_tot - 5.7742095) + 0.7856848", + "lambda_format": "PySRFunction(X=>dCl_tot_dt/(Cl_tot - 5.7742095) + 0.7856848)" + }, + { + "complexity": 10, + "loss": 0.026970413, + "equation": "(dCl_tot_dt / ((Cl_tot + Cl_tot) - target_Cd)) + 0.80081576", + "score": 0.06431138768048111, + "sympy_format": "dCl_tot_dt/(Cl_tot + Cl_tot - target_Cd) + 0.80081576", + "lambda_format": "PySRFunction(X=>dCl_tot_dt/(Cl_tot + Cl_tot - target_Cd) + 0.80081576)" + }, + { + "complexity": 11, + "loss": 0.02025791, + "equation": "((dCl_tot_dt * -0.13315448) * (Cl_tot + bias)) + 0.80922115", + "score": 0.2861951157800486, + "sympy_format": "dCl_tot_dt*(-0.13315448)*(Cl_tot + bias) + 0.80922115", + "lambda_format": "PySRFunction(X=>dCl_tot_dt*(-0.13315448)*(Cl_tot + bias) + 0.80922115)" + }, + { + "complexity": 12, + "loss": 0.0187998, + "equation": "(((Cl_tot + 1.4032292) * dCl_tot_dt) * -0.11100898) + 0.80360377", + "score": 0.0746991030839848, + "sympy_format": "(Cl_tot + 1.4032292)*dCl_tot_dt*(-0.11100898) + 0.80360377", + "lambda_format": "PySRFunction(X=>(Cl_tot + 1.4032292)*dCl_tot_dt*(-0.11100898) + 0.80360377)" + }, + { + "complexity": 13, + "loss": 0.017390246, + "equation": "square(((dCl_tot_dt * (Cl_tot + 1.4040092)) * -0.065458074) + 0.88719195)", + "score": 0.0779367571543523, + "sympy_format": "(dCl_tot_dt*(Cl_tot + 1.4040092)*(-0.065458074) + 0.88719195)**2", + "lambda_format": "PySRFunction(X=>(dCl_tot_dt*(Cl_tot + 1.4040092)*(-0.065458074) + 0.88719195)**2)" + }, + { + "complexity": 14, + "loss": 0.015813593, + "equation": "((dCl_tot_dt * -0.11452771) * ((Cl_err + Cl_tot) + 1.4403551)) + 0.78953165", + "score": 0.09503958769856034, + "sympy_format": "dCl_tot_dt*(-0.11452771)*(Cl_err + Cl_tot + 1.4403551) + 0.78953165", + "lambda_format": "PySRFunction(X=>dCl_tot_dt*(-0.11452771)*(Cl_err + Cl_tot + 1.4403551) + 0.78953165)" + }, + { + "complexity": 15, + "loss": 0.013981272, + "equation": "(((square(Cl_err + 1.2575057) + Cl_tot) * -0.1052603) * dCl_tot_dt) + 0.76740843", + "score": 0.12315116683748104, + "sympy_format": "(Cl_tot + (Cl_err + 1.2575057)**2)*(-0.1052603)*dCl_tot_dt + 0.76740843", + "lambda_format": "PySRFunction(X=>(Cl_tot + (Cl_err + 1.2575057)**2)*(-0.1052603)*dCl_tot_dt + 0.76740843)" + } + ] +} \ No newline at end of file diff --git a/src/SR_analysis/results/formulas/illusion_joint_front.json b/src/SR_analysis/results/formulas/illusion_joint_front.json new file mode 100644 index 0000000..b55c16c --- /dev/null +++ b/src/SR_analysis/results/formulas/illusion_joint_front.json @@ -0,0 +1,19 @@ +{ + "scene": "illusion_joint", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "Cd_err", + "Cl_err", + "dCd_err_dt", + "dCl_err_dt" + ], + "best_sympy": "Cd_tot - (Cd_err + 5.4276643) - (-0.009783858)*(du_a_dt + u_a)", + "best_score": 0.9072 +} \ No newline at end of file diff --git a/src/SR_analysis/results/formulas/illusion_joint_marker_front.json b/src/SR_analysis/results/formulas/illusion_joint_marker_front.json new file mode 100644 index 0000000..b079522 --- /dev/null +++ b/src/SR_analysis/results/formulas/illusion_joint_marker_front.json @@ -0,0 +1,20 @@ +{ + "scene": "illusion_joint_marker", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "Cd_err", + "Cl_err", + "dCd_err_dt", + "dCl_err_dt", + "target_diameter" + ], + "best_sympy": "target_diameter*((du_a_dt + u_a)*0.010333712 + 6.324169) - 5.9960093", + "best_score": 0.9416 +} \ No newline at end of file diff --git a/src/SR_analysis/results/formulas/illusion_joint_top.json b/src/SR_analysis/results/formulas/illusion_joint_top.json new file mode 100644 index 0000000..b4f8b22 --- /dev/null +++ b/src/SR_analysis/results/formulas/illusion_joint_top.json @@ -0,0 +1,20 @@ +{ + "scene": "illusion_joint", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "Cd_err", + "Cl_err", + "dCd_err_dt", + "dCl_err_dt" + ], + "best_sympy": "(Cd_err - (Cd_rear - Cl_err))*0.53502613 + 2.7819264", + "best_score": 0.8279 +} \ No newline at end of file diff --git a/src/SR_analysis/results/formulas/karman_joint_front.json b/src/SR_analysis/results/formulas/karman_joint_front.json new file mode 100644 index 0000000..c95321e --- /dev/null +++ b/src/SR_analysis/results/formulas/karman_joint_front.json @@ -0,0 +1,27 @@ +{ + "scene": "karman_joint", + "scene_id": "karman", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "daF_dt", + "daB_dt", + "daT_dt", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "best_sympy": "daB_dt*0.38504666 + daF_dt + mu_Cl_tot*(-14.951645)", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/results/formulas/karman_joint_top.json b/src/SR_analysis/results/formulas/karman_joint_top.json new file mode 100644 index 0000000..fe18da8 --- /dev/null +++ b/src/SR_analysis/results/formulas/karman_joint_top.json @@ -0,0 +1,28 @@ +{ + "scene": "karman_joint", + "scene_id": "karman", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "daF_dt", + "daB_dt", + "daT_dt", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "best_sympy": "3.4138296", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/results/formulas/karman_re100_front.json b/src/SR_analysis/results/formulas/karman_re100_front.json new file mode 100644 index 0000000..9d85bc9 --- /dev/null +++ b/src/SR_analysis/results/formulas/karman_re100_front.json @@ -0,0 +1,27 @@ +{ + "scene": "karman_re100", + "scene_id": "karman", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "daF_dt", + "daB_dt", + "daT_dt", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "best_sympy": "mu_u_a*(-3.6490004) - (daF_dt - 1*2.3221624)/(Cl_diff - daF_dt)", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/results/formulas/karman_re100_top.json b/src/SR_analysis/results/formulas/karman_re100_top.json new file mode 100644 index 0000000..7d760d1 --- /dev/null +++ b/src/SR_analysis/results/formulas/karman_re100_top.json @@ -0,0 +1,28 @@ +{ + "scene": "karman_re100", + "scene_id": "karman", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "daF_dt", + "daB_dt", + "daT_dt", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "best_sympy": "mu_u_a - (daF_dt + daT_dt*(-0.45103812)) + 4.084246", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/results/formulas/karman_re200_front.json b/src/SR_analysis/results/formulas/karman_re200_front.json new file mode 100644 index 0000000..3ec04a9 --- /dev/null +++ b/src/SR_analysis/results/formulas/karman_re200_front.json @@ -0,0 +1,27 @@ +{ + "scene": "karman_re200", + "scene_id": "karman", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "daF_dt", + "daB_dt", + "daT_dt", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "best_sympy": "daF_dt - (-0.35635605)*(daB_dt + u_a/Cl_diff)", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/results/formulas/karman_re200_top.json b/src/SR_analysis/results/formulas/karman_re200_top.json new file mode 100644 index 0000000..d607324 --- /dev/null +++ b/src/SR_analysis/results/formulas/karman_re200_top.json @@ -0,0 +1,28 @@ +{ + "scene": "karman_re200", + "scene_id": "karman", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "daF_dt", + "daB_dt", + "daT_dt", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "best_sympy": "-daB_dt*mu_u_a + daF_dt/(-0.77701634) + 2.6764865", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/results/formulas/karman_re400_front.json b/src/SR_analysis/results/formulas/karman_re400_front.json new file mode 100644 index 0000000..8569b0e --- /dev/null +++ b/src/SR_analysis/results/formulas/karman_re400_front.json @@ -0,0 +1,27 @@ +{ + "scene": "karman_re400", + "scene_id": "karman", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "daF_dt", + "daB_dt", + "daT_dt", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "best_sympy": "mu_Cl_tot*21.972973 + mu_u_a*(Cl_diff - 1*(-18.038433))", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/results/formulas/karman_re400_top.json b/src/SR_analysis/results/formulas/karman_re400_top.json new file mode 100644 index 0000000..f6e5555 --- /dev/null +++ b/src/SR_analysis/results/formulas/karman_re400_top.json @@ -0,0 +1,28 @@ +{ + "scene": "karman_re400", + "scene_id": "karman", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "daF_dt", + "daB_dt", + "daT_dt", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "best_sympy": "-daF_dt + u_a*(-0.073740624) + 2.584631", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/results/formulas/karman_re50_front.json b/src/SR_analysis/results/formulas/karman_re50_front.json new file mode 100644 index 0000000..a081102 --- /dev/null +++ b/src/SR_analysis/results/formulas/karman_re50_front.json @@ -0,0 +1,27 @@ +{ + "scene": "karman_re50", + "scene_id": "karman", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "daF_dt", + "daB_dt", + "daT_dt", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "best_sympy": "Cd_rear*Cd_tot - 0.5113535*Cl_tot + mu_u_a*(2.8397622 - daB_dt)", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/results/formulas/karman_re50_top.json b/src/SR_analysis/results/formulas/karman_re50_top.json new file mode 100644 index 0000000..2e5577f --- /dev/null +++ b/src/SR_analysis/results/formulas/karman_re50_top.json @@ -0,0 +1,28 @@ +{ + "scene": "karman_re50", + "scene_id": "karman", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "daF_dt", + "daB_dt", + "daT_dt", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "best_sympy": "(0.74166375 - (-0.0649042)*daT_dt)*(Cd_rear - Cd_tot + 5.43748)", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/results/validations/illusion_0.5L.json b/src/SR_analysis/results/validations/illusion_0.5L.json new file mode 100644 index 0000000..a387051 --- /dev/null +++ b/src/SR_analysis/results/validations/illusion_0.5L.json @@ -0,0 +1,8 @@ +{ + "scene": "illusion_0.5L", + "mode": "pysr", + "similarity_full": 0.8539735529506018, + "similarity_tail": 0.8411474326454078, + "action_range": 0.06714382640888465, + "n_steps": 320 +} \ No newline at end of file diff --git a/src/SR_analysis/results/validations/illusion_0.6L.json b/src/SR_analysis/results/validations/illusion_0.6L.json new file mode 100644 index 0000000..1951547 --- /dev/null +++ b/src/SR_analysis/results/validations/illusion_0.6L.json @@ -0,0 +1,8 @@ +{ + "scene": "illusion_0.6L", + "mode": "pysr", + "similarity_full": 0.9388972369369757, + "similarity_tail": 0.8608654259522963, + "action_range": 0.044579733956375364, + "n_steps": 320 +} \ No newline at end of file diff --git a/src/SR_analysis/results/validations/illusion_0.75L.json b/src/SR_analysis/results/validations/illusion_0.75L.json new file mode 100644 index 0000000..d34259f --- /dev/null +++ b/src/SR_analysis/results/validations/illusion_0.75L.json @@ -0,0 +1,8 @@ +{ + "scene": "illusion_0.75L", + "mode": "pysr", + "similarity_full": 0.9821949471763758, + "similarity_tail": 0.8071620435178003, + "action_range": 0.029841950903943048, + "n_steps": 320 +} \ No newline at end of file diff --git a/src/SR_analysis/results/validations/illusion_0.75L_ppo.json b/src/SR_analysis/results/validations/illusion_0.75L_ppo.json new file mode 100644 index 0000000..f7b9f52 --- /dev/null +++ b/src/SR_analysis/results/validations/illusion_0.75L_ppo.json @@ -0,0 +1,5 @@ +{ + "scene": "illusion_0.75L", + "controlled": true, + "similarity": 0.980430435808052 +} \ No newline at end of file diff --git a/src/SR_analysis/results/validations/illusion_0.8L.json b/src/SR_analysis/results/validations/illusion_0.8L.json new file mode 100644 index 0000000..6feb0d9 --- /dev/null +++ b/src/SR_analysis/results/validations/illusion_0.8L.json @@ -0,0 +1,8 @@ +{ + "scene": "illusion_0.8L", + "mode": "pysr", + "similarity_full": 0.9076366235277858, + "similarity_tail": 0.9003715798241534, + "action_range": 0.02705657196270742, + "n_steps": 214 +} \ No newline at end of file diff --git a/src/SR_analysis/results/validations/illusion_1.2L.json b/src/SR_analysis/results/validations/illusion_1.2L.json new file mode 100644 index 0000000..577669a --- /dev/null +++ b/src/SR_analysis/results/validations/illusion_1.2L.json @@ -0,0 +1,8 @@ +{ + "scene": "illusion_1.2L", + "mode": "pysr", + "similarity_full": 0.8485137980740682, + "similarity_tail": 0.7692440834631539, + "action_range": 0.026140261986299245, + "n_steps": 214 +} \ No newline at end of file diff --git a/src/SR_analysis/results/validations/illusion_1.5L_ppo.json b/src/SR_analysis/results/validations/illusion_1.5L_ppo.json new file mode 100644 index 0000000..4b652bd --- /dev/null +++ b/src/SR_analysis/results/validations/illusion_1.5L_ppo.json @@ -0,0 +1,5 @@ +{ + "scene": "illusion_1.5L", + "controlled": true, + "similarity": 0.9453180853373244 +} \ No newline at end of file diff --git a/src/SR_analysis/results/validations/illusion_1L.json b/src/SR_analysis/results/validations/illusion_1L.json new file mode 100644 index 0000000..b0d14e4 --- /dev/null +++ b/src/SR_analysis/results/validations/illusion_1L.json @@ -0,0 +1,8 @@ +{ + "scene": "illusion_1L", + "mode": "pysr", + "similarity_full": 0.9580609580562278, + "similarity_tail": 0.8857815231916633, + "action_range": 0.011634223590664046, + "n_steps": 214 +} \ No newline at end of file diff --git a/src/SR_analysis/results/validations/illusion_1L_ppo.json b/src/SR_analysis/results/validations/illusion_1L_ppo.json new file mode 100644 index 0000000..21bc61c --- /dev/null +++ b/src/SR_analysis/results/validations/illusion_1L_ppo.json @@ -0,0 +1,5 @@ +{ + "scene": "illusion_1L", + "controlled": true, + "similarity": 0.9753732477509874 +} \ No newline at end of file diff --git a/src/SR_analysis/results/validations/illusion_1L_target.json b/src/SR_analysis/results/validations/illusion_1L_target.json new file mode 100644 index 0000000..b0d14e4 --- /dev/null +++ b/src/SR_analysis/results/validations/illusion_1L_target.json @@ -0,0 +1,8 @@ +{ + "scene": "illusion_1L", + "mode": "pysr", + "similarity_full": 0.9580609580562278, + "similarity_tail": 0.8857815231916633, + "action_range": 0.011634223590664046, + "n_steps": 214 +} \ No newline at end of file diff --git a/src/SR_analysis/results/validations/illusion_2L.json b/src/SR_analysis/results/validations/illusion_2L.json new file mode 100644 index 0000000..dfa58aa --- /dev/null +++ b/src/SR_analysis/results/validations/illusion_2L.json @@ -0,0 +1,8 @@ +{ + "scene": "illusion_2L", + "mode": "pysr", + "similarity_full": 0.6754607445898861, + "similarity_tail": 0.6292225479264744, + "action_range": 0.3686004659586289, + "n_steps": 160 +} \ No newline at end of file diff --git a/src/SR_analysis/results/validations/karman_re100.json b/src/SR_analysis/results/validations/karman_re100.json new file mode 100644 index 0000000..4a42bdb --- /dev/null +++ b/src/SR_analysis/results/validations/karman_re100.json @@ -0,0 +1,7 @@ +{ + "scene": "karman_re100", + "mode": "pysr", + "similarity": 0.8876964046102431, + "action_range": 0.034138296000000005, + "n_steps": 200 +} \ No newline at end of file diff --git a/src/SR_analysis/results/validations/karman_re100_ppo.json b/src/SR_analysis/results/validations/karman_re100_ppo.json new file mode 100644 index 0000000..dbdc6a6 --- /dev/null +++ b/src/SR_analysis/results/validations/karman_re100_ppo.json @@ -0,0 +1,6 @@ +{ + "scene": "karman_re100", + "controlled": true, + "avg_reward_last100": 0.6665451352155793, + "similarity": 0.9538050162761162 +} \ No newline at end of file diff --git a/src/SR_analysis/results/validations/karman_re200.json b/src/SR_analysis/results/validations/karman_re200.json new file mode 100644 index 0000000..42119cc --- /dev/null +++ b/src/SR_analysis/results/validations/karman_re200.json @@ -0,0 +1,7 @@ +{ + "scene": "karman_re200", + "mode": "pysr", + "similarity": 0.8448181642866177, + "action_range": 0.034138296000000005, + "n_steps": 200 +} \ No newline at end of file diff --git a/src/SR_analysis/results/validations/karman_re200_ppo.json b/src/SR_analysis/results/validations/karman_re200_ppo.json new file mode 100644 index 0000000..71b69f3 --- /dev/null +++ b/src/SR_analysis/results/validations/karman_re200_ppo.json @@ -0,0 +1,6 @@ +{ + "scene": "karman_re200", + "controlled": true, + "avg_reward_last100": 0.3298547239579881, + "similarity": 0.8842106151498026 +} \ No newline at end of file diff --git a/src/SR_analysis/results/validations/karman_re400.json b/src/SR_analysis/results/validations/karman_re400.json new file mode 100644 index 0000000..0375b48 --- /dev/null +++ b/src/SR_analysis/results/validations/karman_re400.json @@ -0,0 +1,7 @@ +{ + "scene": "karman_re400", + "mode": "pysr", + "similarity": 0.8058735756048312, + "action_range": 0.034138296000000005, + "n_steps": 200 +} \ No newline at end of file diff --git a/src/SR_analysis/results/validations/karman_re400_SI400.json b/src/SR_analysis/results/validations/karman_re400_SI400.json new file mode 100644 index 0000000..9f70d25 --- /dev/null +++ b/src/SR_analysis/results/validations/karman_re400_SI400.json @@ -0,0 +1,7 @@ +{ + "scene": "karman_re400", + "mode": "pysr", + "similarity": 0.8185568187903199, + "action_range": 0.06124516065871972, + "n_steps": 320 +} \ No newline at end of file diff --git a/src/SR_analysis/results/validations/karman_re400_ppo.json b/src/SR_analysis/results/validations/karman_re400_ppo.json new file mode 100644 index 0000000..79569e0 --- /dev/null +++ b/src/SR_analysis/results/validations/karman_re400_ppo.json @@ -0,0 +1,7 @@ +{ + "re_code": 400, + "uncontrolled": true, + "controlled": true, + "avg_reward_last100": 0.4389868174760177, + "similarity": 0.7950085552241137 +} \ No newline at end of file diff --git a/src/SR_analysis/results/validations/karman_re50.json b/src/SR_analysis/results/validations/karman_re50.json new file mode 100644 index 0000000..513985a --- /dev/null +++ b/src/SR_analysis/results/validations/karman_re50.json @@ -0,0 +1,7 @@ +{ + "scene": "karman_re50", + "mode": "pysr", + "similarity": 0.8468594520598547, + "action_range": 0.15152156686076074, + "n_steps": 200 +} \ No newline at end of file diff --git a/src/SR_analysis/results/validations/karman_re50_ppo.json b/src/SR_analysis/results/validations/karman_re50_ppo.json new file mode 100644 index 0000000..ed32140 --- /dev/null +++ b/src/SR_analysis/results/validations/karman_re50_ppo.json @@ -0,0 +1,6 @@ +{ + "scene": "karman_re50", + "controlled": true, + "avg_reward_last100": 0.5020737935656798, + "similarity": 0.9614716421942122 +} \ No newline at end of file diff --git a/src/SR_analysis/results/validations/vortex_lamb.json b/src/SR_analysis/results/validations/vortex_lamb.json new file mode 100644 index 0000000..ebc5c7d --- /dev/null +++ b/src/SR_analysis/results/validations/vortex_lamb.json @@ -0,0 +1,8 @@ +{ + "scene": "vortex_lamb", + "mode": "pysr", + "similarity": 0.9489728204369429, + "action_range": 0.034138296000000005, + "n_steps": 150, + "threshold": "best" +} \ No newline at end of file diff --git a/src/SR_analysis/results/validations/vortex_lamb_ppo.json b/src/SR_analysis/results/validations/vortex_lamb_ppo.json new file mode 100644 index 0000000..11e2dbf --- /dev/null +++ b/src/SR_analysis/results/validations/vortex_lamb_ppo.json @@ -0,0 +1,5 @@ +{ + "scene": "vortex_lamb", + "controlled": true, + "similarity": 0.9421189523977421 +} \ No newline at end of file diff --git a/src/SR_analysis/results/validations/vortex_taylor.json b/src/SR_analysis/results/validations/vortex_taylor.json new file mode 100644 index 0000000..f552132 --- /dev/null +++ b/src/SR_analysis/results/validations/vortex_taylor.json @@ -0,0 +1,8 @@ +{ + "scene": "vortex_taylor", + "mode": "pysr", + "similarity": 0.9044944003424866, + "action_range": 0.034138296000000005, + "n_steps": 150, + "threshold": "best" +} \ No newline at end of file diff --git a/src/SR_analysis/results/validations/vortex_taylor_ppo.json b/src/SR_analysis/results/validations/vortex_taylor_ppo.json new file mode 100644 index 0000000..b6c321e --- /dev/null +++ b/src/SR_analysis/results/validations/vortex_taylor_ppo.json @@ -0,0 +1,5 @@ +{ + "scene": "vortex_taylor", + "controlled": true, + "similarity": 0.9158487825490536 +} \ No newline at end of file diff --git a/src/SR_analysis/scene_registry.json b/src/SR_analysis/scene_registry.json new file mode 100644 index 0000000..2d063ee --- /dev/null +++ b/src/SR_analysis/scene_registry.json @@ -0,0 +1,304 @@ +{ + "generated": "2026-06-28", + "pipeline": "SR_analysis", + "scenes": { + "karman_re50": { + "formulas": { + "front": "results/formulas/karman_joint_front.json", + "top": "results/formulas/karman_joint_top.json" + }, + "validations": { + "pysr_joint": { + "file": "results/validations/karman_re50.json", + "similarity": 0.8468594520598547 + } + }, + "ppo_similarity": 0.9614716421942122 + }, + "karman_re100": { + "formulas": { + "front": "results/formulas/karman_joint_front.json", + "top": "results/formulas/karman_joint_top.json" + }, + "validations": { + "pysr_joint": { + "file": "results/validations/karman_re100.json", + "similarity": 0.8876964046102431 + } + }, + "ppo_similarity": 0.9538050162761162 + }, + "karman_re200": { + "formulas": { + "front": "results/formulas/karman_joint_front.json", + "top": "results/formulas/karman_joint_top.json" + }, + "validations": { + "pysr_joint": { + "file": "results/validations/karman_re200.json", + "similarity": 0.8448181642866177 + } + }, + "ppo_similarity": 0.8842106151498026 + }, + "karman_re400": { + "formulas": { + "front": "results/formulas/karman_joint_front.json", + "top": "results/formulas/karman_joint_top.json" + }, + "validations": { + "pysr_joint": { + "file": "results/validations/karman_re400.json", + "similarity": 0.8058735756048312 + }, + "pysr_joint_SI400": { + "file": "results/validations/karman_re400_SI400.json", + "similarity": 0.8185568187903199 + } + }, + "ppo_similarity": 0.7950085552241137 + }, + "illusion_0.5L": { + "formulas": { + "front": "results/formulas/illusion_joint_front.json" + }, + "validations": { + "pysr_joint": { + "file": "results/validations/illusion_0.5L.json", + "similarity": 0.8539735529506018 + } + } + }, + "illusion_0.6L": { + "formulas": { + "front": "results/formulas/illusion_joint_front.json" + }, + "validations": { + "pysr_joint": { + "file": "results/validations/illusion_0.6L.json", + "similarity": 0.9388972369369757 + } + } + }, + "illusion_0.75L": { + "formulas": { + "front": "results/formulas/illusion_0.75L_front.json", + "top": "results/formulas/illusion_0.75L_top.json", + "joint_front": "results/formulas/illusion_joint_front.json", + "joint_top": "results/formulas/illusion_joint_top.json", + "target_front": "results/formulas/illusion_0.75L_front_target.json", + "target_top": "results/formulas/illusion_0.75L_top_target.json" + }, + "validations": { + "pysr_joint": { + "file": "results/validations/illusion_0.75L.json", + "similarity": 0.9821949471763758 + } + }, + "ppo_similarity": 0.980430435808052 + }, + "illusion_0.8L": { + "formulas": { + "front": "results/formulas/illusion_joint_front.json" + }, + "validations": { + "pysr_joint": { + "file": "results/validations/illusion_0.8L.json", + "similarity": 0.9076366235277858 + } + } + }, + "illusion_1L": { + "formulas": { + "front": "results/formulas/illusion_1L_front.json", + "top": "results/formulas/illusion_1L_top.json", + "joint_front": "results/formulas/illusion_joint_front.json", + "joint_top": "results/formulas/illusion_joint_top.json", + "marker_front": "results/formulas/illusion_joint_marker_front.json", + "target_front": "results/formulas/illusion_1L_front_target.json", + "target_top": "results/formulas/illusion_1L_top_target.json" + }, + "validations": { + "pysr_joint": { + "file": "results/validations/illusion_1L.json", + "similarity": 0.9580609580562278 + }, + "pysr_target": { + "file": "results/validations/illusion_1L_target.json", + "similarity": 0.9580609580562278 + } + }, + "ppo_similarity": 0.9753732477509874 + }, + "illusion_1.2L": { + "formulas": { + "front": "results/formulas/illusion_joint_front.json" + }, + "validations": { + "pysr_joint": { + "file": "results/validations/illusion_1.2L.json", + "similarity": 0.8485137980740682 + } + } + }, + "illusion_1.5L": { + "formulas": { + "front": "results/formulas/illusion_1.5L_front.json" + }, + "note": "Not SR-amenable; high-frequency modulation", + "ppo_similarity": 0.9453180853373244 + }, + "illusion_2L": { + "formulas": { + "front": "results/formulas/illusion_joint_front.json" + }, + "validations": { + "pysr_joint": { + "file": "results/validations/illusion_2L.json", + "similarity": 0.6754607445898861 + } + } + }, + "vortex_lamb": { + "formulas": { + "front": "results/formulas/karman_joint_front.json" + }, + "validations": { + "pysr_joint": { + "file": "results/validations/vortex_lamb.json", + "similarity": 0.9489728204369429 + } + }, + "ppo_similarity": 0.9421189523977421 + }, + "vortex_taylor": { + "formulas": { + "front": "results/formulas/karman_joint_front.json" + }, + "validations": { + "pysr_joint": { + "file": "results/validations/vortex_taylor.json", + "similarity": 0.9044944003424866 + } + }, + "ppo_similarity": 0.9158487825490536 + } + }, + "formula_files": { + "karman_joint_front": { + "formula": "daB_dt*0.38504666 + daF_dt + mu_Cl_tot*(-14.951645)", + "r2": 1.0, + "canonical": true + }, + "karman_joint_top": { + "formula": "3.4138296", + "r2": 1.0, + "canonical": true + }, + "illusion_joint_front": { + "formula": "Cd_tot - (Cd_err + 5.4276643) - (-0.009783858)*(du_a_dt + u_a)", + "r2": 0.9072, + "canonical": true + }, + "illusion_joint_top": { + "formula": "(Cd_err - (Cd_rear - Cl_err))*0.53502613 + 2.7819264", + "r2": 0.8279, + "canonical": true + }, + "karman_re50_front": { + "formula": "Cd_rear*Cd_tot - 0.5113535*Cl_tot + mu_u_a*(2.8397622 - daB_dt)", + "r2": 1.0, + "canonical": true + }, + "karman_re50_top": { + "formula": "(0.74166375 - (-0.0649042)*daT_dt)*(Cd_rear - Cd_tot + 5.43748)", + "r2": 1.0, + "canonical": true + }, + "karman_re100_front": { + "formula": "mu_u_a*(-3.6490004) - (daF_dt - 1*2.3221624)/(Cl_diff - daF_dt)", + "r2": 1.0, + "canonical": true + }, + "karman_re100_top": { + "formula": "mu_u_a - (daF_dt + daT_dt*(-0.45103812)) + 4.084246", + "r2": 1.0, + "canonical": true + }, + "karman_re200_front": { + "formula": "daF_dt - (-0.35635605)*(daB_dt + u_a/Cl_diff)", + "r2": 1.0, + "canonical": true + }, + "karman_re200_top": { + "formula": "-daB_dt*mu_u_a + daF_dt/(-0.77701634) + 2.6764865", + "r2": 1.0, + "canonical": true + }, + "karman_re400_front": { + "formula": "mu_Cl_tot*21.972973 + mu_u_a*(Cl_diff - 1*(-18.038433))", + "r2": 1.0, + "canonical": true + }, + "karman_re400_top": { + "formula": "-daF_dt + u_a*(-0.073740624) + 2.584631", + "r2": 1.0, + "canonical": true + }, + "illusion_0.75L_front": { + "formula": "-0.16933453*(Cl_tot + dCl_tot_dt) - 1.2398432", + "r2": 0.6917657052732269, + "canonical": true + }, + "illusion_0.75L_top": { + "formula": "Cd_err*0.5409948 + Cl_err + u_a*(-0.015207872) + 1.9898309", + "r2": 0.8559884564869915, + "canonical": true + }, + "illusion_1L_front": { + "formula": "(du_a_dt + u_a + 26.506319)*0.012334255", + "r2": 0.9288424393884038, + "canonical": true + }, + "illusion_1L_top": { + "formula": "0.7962736 - dCl_tot_dt*(Cl_tot + bias)/(Cd_rear + Cd_rear)", + "r2": 0.6959860204249317, + "canonical": true + }, + "illusion_0.75L_front_target": { + "formula": "-(dCl_tot_dt + target_Cl)/5.8401647 - 1.2372783", + "r2": 0.7232722304555419, + "canonical": true + }, + "illusion_0.75L_top_target": { + "formula": "Cd_tot*0.45037818 + Cl_err - 0.016348997*u_a", + "r2": 0.8375927169907114, + "canonical": true + }, + "illusion_1L_front_target": { + "formula": "(du_a_dt + u_a + 26.506351)*0.012334249", + "r2": 0.9288424393879982, + "canonical": true + }, + "illusion_1L_top_target": { + "formula": "dCl_tot_dt*(-0.13315448)*(Cl_tot + bias) + 0.80922115", + "r2": 0.6947513140376443, + "canonical": true + }, + "illusion_joint_marker_front": { + "formula": "target_diameter*((du_a_dt + u_a)*0.010333712 + 6.324169) - 5.9960093", + "r2": 0.9416, + "canonical": true + }, + "illusion_1.5L_front": { + "formula": "aF_lag1*0.01", + "r2": 1.0, + "canonical": true + }, + "illusion_1.5L_top": { + "formula": "(aT_lag1 + aT_lag1)*0.005", + "r2": 1.0, + "canonical": true + } + } +} \ No newline at end of file diff --git a/src/SR_analysis/scripts/analyze_illusion_degradation.py b/src/SR_analysis/scripts/analyze_illusion_degradation.py new file mode 100644 index 0000000..ef72cdf --- /dev/null +++ b/src/SR_analysis/scripts/analyze_illusion_degradation.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python3 +"""Analyze Illusion degradation: compare PPO strategies across diameters. + +Pure analysis of PPO controlled.npz data — no CFD needed. +Computes FFT spectra, action statistics, investigates joint formula +degradation mechanism without running closed-loop validations. + +Usage: + conda run -n pycuda_3_10 python scripts/analyze_illusion_degradation.py +""" +from __future__ import annotations + +import os +import sys +import numpy as np +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt + +_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) +if _REPO not in sys.path: + sys.path.insert(0, _REPO) +_SRC = os.path.join(_REPO, "src") +if _SRC not in sys.path: + sys.path.insert(0, _SRC) + +from SR_analysis.configs import get_scene, _illusion_key as scene_key + +DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data", "illusion") +OUT_DIR = os.path.join(os.path.dirname(__file__), "..", "data", "degradation_analysis") +os.makedirs(OUT_DIR, exist_ok=True) + +U0 = 0.01 +D_CYL = 20.0 + + +def _fmt_diam(d: float) -> str: + s = f"{d:.3f}".rstrip("0").rstrip(".") + return f"{s}L" + + +def load_ppo_data(diam: float) -> dict: + key = f"illusion_{_fmt_diam(diam)}" + path = os.path.join(DATA_DIR, key, "controlled.npz") + npz = np.load(path, allow_pickle=True) + return { + "sensors": npz["sensors"].astype(np.float64), + "forces": npz["forces"].astype(np.float64), + "actions": npz["actions"].astype(np.float64), # normalized [-1,1] + } + + +def compute_dimless(sensors, forces, u0=U0, d=D_CYL, rho=1.0): + s = np.asarray(sensors, dtype=np.float64) + f = np.asarray(forces, dtype=np.float64) + return { + "u_hat_T": s[:, 0] / u0, "v_hat_T": s[:, 1] / u0, + "u_hat_C": s[:, 2] / u0, "v_hat_C": s[:, 3] / u0, + "u_hat_B": s[:, 4] / u0, "v_hat_B": s[:, 5] / u0, + "Cd_F": 2 * f[:, 0] / (rho * u0**2 * d), + "Cl_F": 2 * f[:, 1] / (rho * u0**2 * d), + "Cd_B": 2 * f[:, 2] / (rho * u0**2 * d), + "Cl_B": 2 * f[:, 3] / (rho * u0**2 * d), + "Cd_T": 2 * f[:, 4] / (rho * u0**2 * d), + "Cl_T": 2 * f[:, 5] / (rho * u0**2 * d), + } + + +def compute_features(dim, a_phys): + """Compute dimensionless features from PPO data.""" + uB, uC, uT = dim["u_hat_B"], dim["u_hat_C"], dim["u_hat_T"] + vB, vC, vT = dim["v_hat_B"], dim["v_hat_C"], dim["v_hat_T"] + CdF, CdT, CdB = dim["Cd_F"], dim["Cd_T"], dim["Cd_B"] + ClF, ClT, ClB = dim["Cl_F"], dim["Cl_T"], dim["Cl_B"] + + feat = {} + feat["u_m"] = (uB + uC + uT) / 3 + feat["u_a"] = (uT - uB) / 2 + feat["u_c"] = uC + feat["v_a"] = (vT - vB) / 2 + feat["Cd_tot"] = CdF + CdT + CdB + feat["Cd_rear"] = CdT + CdB + feat["Cl_tot"] = ClF + ClT + ClB + feat["Cl_diff"] = ClT - ClB + return feat + + +def main(): + print("=" * 60) + print("Illusion Degradation Analysis") + print("=" * 60) + + # 1. Load PPO data for all three trained diameters + ppo = {} + for d in [0.75, 1.0, 1.5]: + ppo[d] = load_ppo_data(d) + cfg = get_scene(scene_key(d)) + a = ppo[d]["actions"] + # Convert normalized actions to physical alpha + scale = cfg["action_scale"] + bias = np.array(cfg["action_bias"]) + a_phys = (a * scale + bias) * cfg["u0"] + alpha = a_phys / cfg["u0"] + print(f"\n--- {d}L, SI={cfg['sample_interval']} ---") + print(f" Steps: {a.shape[0]}") + print(f" Alpha (front): mean={alpha[:,0].mean():.3f}, std={alpha[:,0].std():.3f}, " + f"range=[{alpha[:,0].min():.2f}, {alpha[:,0].max():.2f}]") + print(f" Alpha (bottom): mean={alpha[:,1].mean():.3f}, std={alpha[:,1].std():.3f}, " + f"range=[{alpha[:,1].min():.2f}, {alpha[:,1].max():.2f}]") + print(f" Alpha (top): mean={alpha[:,2].mean():.3f}, std={alpha[:,2].std():.3f}, " + f"range=[{alpha[:,2].min():.2f}, {alpha[:,2].max():.2f}]") + ppo[d]["alpha"] = alpha + ppo[d]["cfg"] = cfg + + # 2. FFT spectral analysis + print("\n" + "=" * 60) + print("FFT Spectral Analysis") + print("=" * 60) + + fig, axes = plt.subplots(3, 3, figsize=(16, 12)) + for di, d in enumerate([0.75, 1.0, 1.5]): + alpha = ppo[d]["alpha"] + for ci, name in enumerate(["Front", "Bottom", "Top"]): + ax = axes[di, ci] + signal = alpha[:, ci] - alpha[:, ci].mean() + fft = np.abs(np.fft.rfft(signal)) + freqs = np.fft.rfftfreq(len(signal), d=1) + ax.plot(freqs[1:], fft[1:], "b-", alpha=0.8) + # Mark dominant frequency + dom_idx = np.argmax(fft[1:]) + 1 + dom_freq = freqs[dom_idx] + ax.axvline(dom_freq, color="r", linestyle="--", alpha=0.5, + label=f"f={dom_freq:.4f}") + ax.set_title(f"{d}L {name}") + ax.set_xlabel("Frequency (control steps)") + ax.set_ylabel("|FFT|") + ax.legend(fontsize=7) + print(f" {d}L {name}: dominant f={dom_freq:.4f} (T={1/dom_freq:.1f} steps)") + plt.tight_layout() + plt.savefig(os.path.join(OUT_DIR, "fft_spectra.png"), dpi=150) + plt.close() + print(f" Saved: fft_spectra.png") + + # 3. Action timeseries comparison + print("\n" + "=" * 60) + print("Action Timeseries (first 100 steps)") + print("=" * 60) + fig, axes = plt.subplots(3, 1, figsize=(14, 10)) + for di, d in enumerate([0.75, 1.0, 1.5]): + ax = axes[di] + alpha = ppo[d]["alpha"] + n_show = min(100, alpha.shape[0]) + ax.plot(alpha[:n_show, 0], label="Front", linewidth=0.8) + ax.plot(alpha[:n_show, 1], label="Bottom", linewidth=0.8) + ax.plot(alpha[:n_show, 2], label="Top", linewidth=0.8) + ax.set_title(f"{d}L — PPO Alpha (non-dim)") + ax.legend() + ax.set_ylabel("α = ω/U₀") + ax.set_xlabel("Step") + plt.tight_layout() + plt.savefig(os.path.join(OUT_DIR, "action_timeseries.png"), dpi=150) + plt.close() + print(f" Saved: action_timeseries.png") + + # 4. Action autocorrelation (detects periodicity pattern) + print("\n" + "=" * 60) + print("Action Autocorrelation") + print("=" * 60) + fig, axes = plt.subplots(3, 3, figsize=(16, 12)) + for di, d in enumerate([0.75, 1.0, 1.5]): + alpha = ppo[d]["alpha"] + for ci, name in enumerate(["Front", "Bottom", "Top"]): + ax = axes[di, ci] + signal = alpha[:, ci] - alpha[:, ci].mean() + # Compute autocorrelation manually + n = len(signal) + ac = np.correlate(signal, signal, mode="full")[n - 1:] + ac = ac / ac[0] + max_lag = min(30, n // 2) + ax.stem(range(max_lag), ac[:max_lag]) + ax.set_title(f"{d}L {name}") + ax.set_xlabel("Lag (steps)") + ax.set_ylabel("ACF") + # Flag lag-2 autocorrelation + if max_lag >= 2: + ac2 = ac[2] + if abs(ac2) > 0.5: + ax.annotate(f"lag-2: {ac2:.2f}", xy=(2, ac2), + fontsize=8, color="red") + plt.tight_layout() + plt.savefig(os.path.join(OUT_DIR, "autocorrelation.png"), dpi=150) + plt.close() + print(f" Saved: autocorrelation.png") + + # 5. Joint formula evaluation + print("\n" + "=" * 60) + print("Joint Formula Range Analysis") + print("=" * 60) + + for d in [0.75, 1.0, 1.5]: + cfg = ppo[d]["cfg"] + sensors = ppo[d]["sensors"] + forces = ppo[d]["forces"] + dim = compute_dimless(sensors, forces) + feat = compute_features(dim, ppo[d]["alpha"] * U0) + + # target_Cd component of joint formula + # For trained scenes we don't have a separate target recording + # but target_diameter defines the expected Cd range + target_diam = cfg["target_diameter"] + # Approximate target_Cd from the feature data (Cd_tot in uncontrolled would give target-like values) + # Get the range of Cd_tot + print(f"\n {d}L (target diam={target_diam}*L0):") + print(f" Cd_tot: mean={feat['Cd_tot'].mean():.4f}, std={feat['Cd_tot'].std():.4f}, " + f"range=[{feat['Cd_tot'].min():.3f}, {feat['Cd_tot'].max():.3f}]") + print(f" Cl_tot: mean={feat['Cl_tot'].mean():.4f}, std={feat['Cl_tot'].std():.4f}, " + f"range=[{feat['Cl_tot'].min():.3f}, {feat['Cl_tot'].max():.3f}]") + print(f" u_a: mean={feat['u_a'].mean():.4f}, std={feat['u_a'].std():.4f}, " + f"range=[{feat['u_a'].min():.3f}, {feat['u_a'].max():.3f}]") + print(f" Cd_rear: mean={feat['Cd_rear'].mean():.4f}, std={feat['Cd_rear'].std():.4f}") + + # 6. Joint formula term breakdown for 0.75L and 1L + print("\n" + "=" * 60) + print("Joint Formula Term Decomposition") + print("=" * 60) + + for d in [0.75, 1.0]: + cfg = ppo[d]["cfg"] + sensors = ppo[d]["sensors"] + forces = ppo[d]["forces"] + a = ppo[d]["alpha"] + n_warmup = 2 + dim = compute_dimless(sensors, forces) + feat = compute_features(dim, a * U0) + + # Joint formula: alpha_F = target_Cd - 5.428 + 0.0098*(du_a_dt + u_a) + # We don't have target_Cd directly, but Cd_tot from PPO trajectory + # approximates it. Let's use Cd_tot as a proxy for target_Cd. + # Joint formula: alpha_F = Cd_tot - (Cd_tot - target_Cd) - 5.428 + 0.0098*(du_a_dt + u_a) + # = target_Cd - 5.428 + 0.0098*(du_a_dt + u_a) + + # Approximate: use Cd_tot mean as proxy + target_cd_proxy = np.mean(feat["Cd_tot"][n_warmup:]) + du_a_dt_raw = np.zeros_like(feat["u_a"]) + du_a_dt_raw[1:] = feat["u_a"][1:] - feat["u_a"][:-1] + + term_drag = target_cd_proxy - 5.428 + term_phase = 0.0098 * (du_a_dt_raw[n_warmup:] + feat["u_a"][n_warmup:]) + alpha_sr = term_drag + term_phase + alpha_ppo = a[n_warmup:, 0] + + print(f"\n {d}L joint formula breakdown (front):") + print(f" target_Cd proxy: {target_cd_proxy:.4f}") + print(f" drag term (target_Cd - 5.428): {term_drag:.4f} (constant)") + print(f" phase term mean: {term_phase.mean():.4f}, std: {term_phase.std():.4f}") + print(f" SR alpha mean: {alpha_sr.mean():.4f}, std: {alpha_sr.std():.4f}") + print(f" PPO alpha mean: {alpha_ppo.mean():.4f}, std: {alpha_ppo.std():.4f}") + print(f" Correlation(SR, PPO): {np.corrcoef(alpha_sr, alpha_ppo)[0,1]:.4f}") + print(f" SR contribution: drag={abs(term_drag):.3f}, phase_std={term_phase.std():.3f}") + + print(f"\n=== Done ===") + print(f"Output: {OUT_DIR}") + + +if __name__ == "__main__": + main() diff --git a/src/SR_analysis/scripts/diagnose_illusion.py b/src/SR_analysis/scripts/diagnose_illusion.py new file mode 100644 index 0000000..f4545a7 --- /dev/null +++ b/src/SR_analysis/scripts/diagnose_illusion.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +"""Diagnose Illusion PPO data and PySR fit quality. + +Checks: + 1. PPO data integrity (sensor ranges, action ranges, similarity match) + 2. G-equivariance of PPO policy (should front be odd? rear shared-head?) + 3. Front constant behavior at 0.75L — is this real physics or fitting artifact? + 4. PySR front no-bias constraint — does the fitting actually enforce it? + 5. Phase portraits (sensor vs sensor) to check for periodic signals + 6. Action timeseries over multiple periods (not truncated) + +Usage: + conda run -n pycuda_3_10 python scripts/diagnose_illusion.py +""" +from __future__ import annotations + +import json +import os +import sys +import numpy as np +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt + +_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) +if _REPO not in sys.path: + sys.path.insert(0, _REPO) +_SRC = os.path.join(_REPO, "src") +if _SRC not in sys.path: + sys.path.insert(0, _SRC) + +from SR_analysis.configs import get_scene + +U0 = 0.01 +DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data", "illusion") +FIG_DIR = os.path.join(os.path.dirname(__file__), "..", "data", "diagnostics") +os.makedirs(FIG_DIR, exist_ok=True) + + +def load_scene(scene_name): + path = os.path.join(DATA_DIR, scene_name, "controlled.npz") + cfg = get_scene(scene_name) + npz = np.load(path, allow_pickle=True) + sensors = npz["sensors"].astype(np.float64) + forces = npz["forces"].astype(np.float64) + actions_norm = npz["actions"].astype(np.float64) + scale = cfg["action_scale"] + bias = np.array(cfg["action_bias"], dtype=np.float64) + actions_phys = (actions_norm * scale + bias) * cfg["u0"] + alpha = actions_phys / cfg["u0"] + return sensors, forces, actions_norm, alpha, cfg + + +def check_similarity_vs_training(scene_name): + sensors, forces, actions_norm, alpha, cfg = load_scene(scene_name) + result_path = os.path.join(DATA_DIR, scene_name, "result.json") + if os.path.exists(result_path): + result = json.load(open(result_path)) + sim = result.get("similarity", None) + if sim is not None: + print(f" PPO similarity (controlled.npz): {sim:.4f}") + else: + print(f" result.json keys: {list(result.keys())}") + else: + print(f" WARNING: no result.json for {scene_name}") + return + + target_path = os.path.join(DATA_DIR, scene_name, "target.npz") + if os.path.exists(target_path): + target = np.load(target_path)["target_states"] + T = min(len(sensors), len(target)) + for i, name in enumerate(["S0_ux", "S0_uy", "S1_ux", "S1_uy", "S2_ux", "S2_uy"]): + sens = sensors[:T, i] + targ = target[:T, 2+i] if target.shape[1] >= 8 else sensors[:T, i] + if i == 0: + print(f" {name}: ctl_mean={sens.mean():.6f}, ctl_std={sens.std():.6f}") + if i <= 1: + pass # already printed + # Print summary + print(f" Controlled sensors RMS: {np.sqrt(np.mean(sensors[:T,:]**2)):.6f}") + if np.any(np.isnan(sensors)): + print(f" ERROR: NaN in sensors!") + if np.any(np.isnan(forces)): + print(f" ERROR: NaN in forces!") + + +def check_actions(alpha, scene_name): + print(f"\n {scene_name} alpha (non-dim omega/U0):") + for i, name in enumerate(["Front", "Bottom", "Top"]): + a = alpha[:, i] + print(f" {name}: mu={a.mean():+.3f}, sigma={a.std():.3f}, min={a.min():+.2f}, max={a.max():+.2f}") + sat_rate = np.mean(np.abs(a) > 7.9) + if sat_rate > 0.05: + print(f" WARNING: {sat_rate*100:.1f}% near saturation") + + front = alpha[:, 0] + if front.std() > 0 and abs(front.mean()) > 0: + rel_std = front.std() / (abs(front.mean()) + 1e-6) + if rel_std < 0.3: + print(f" NOTE: Front nearly constant (relative std = {rel_std:.2f})") + + +def check_gee_equivariance(alpha, sensors, forces, scene_name): + from SR_analysis.utils.g_operator import apply_G_raw + + T = min(len(alpha), 150) + alpha_front = alpha[:, 0] + u_a_simple = (sensors[:, 0] - sensors[:, 4]) / 2 # s0_ux - s2_ux + + u_a_pos = u_a_simple[2:T] > np.percentile(u_a_simple[2:T], 80) + u_a_neg = u_a_simple[2:T] < np.percentile(u_a_simple[2:T], 20) + + front_pos = alpha_front[2:T][u_a_pos] + front_neg = alpha_front[2:T][u_a_neg] + + if len(front_pos) > 0 and len(front_neg) > 0: + print(f" {scene_name}:") + print(f" Front when u_a > P80 (top fast, bottom slow): mu={front_pos.mean():+.3f}") + print(f" Front when u_a < P20 (bottom fast, top slow): mu={front_neg.mean():+.3f}") + delta = abs(front_pos.mean() - front_neg.mean()) + scale = max(abs(front_pos.mean()), abs(front_neg.mean()), 1.0) + if delta < 0.5 * scale: + print(f" → Front nearly symmetric to asymmetry (G-odd behavior weak)") + else: + print(f" → Front responds to asymmetry (consistent with G-odd)") + else: + print(f" {scene_name}: not enough data for G-check") + + +def plot_phase_portraits(sensors, scene_name): + fig, axes = plt.subplots(1, 3, figsize=(18, 5)) + names = [("Top", 0, 1), ("Center", 2, 3), ("Bottom", 4, 5)] + for ax, (label, ui, vi) in zip(axes, names): + ax.plot(sensors[:, ui], sensors[:, vi], linewidth=0.3, alpha=0.7) + ax.set_title(f"{scene_name} — {label} Sensor") + ax.set_xlabel("ux (lattice)") + ax.set_ylabel("uy (lattice)") + ax.set_aspect("equal") + plt.tight_layout() + out = os.path.join(FIG_DIR, f"phase_portrait_{scene_name}.png") + plt.savefig(out, dpi=120) + plt.close() + print(f" Saved: {os.path.basename(out)}") + + +def plot_full_timeseries(alpha, scene_name, n_steps=200): + n_plot = min(n_steps, alpha.shape[0]) + fig, axes = plt.subplots(3, 1, figsize=(16, 10), sharex=True) + colors = ["blue", "green", "red"] + names = ["Front", "Bottom", "Top"] + for ax, ci, name, c in zip(axes, range(3), names, colors): + ax.plot(range(n_plot), alpha[:n_plot, ci], color=c, linewidth=0.6) + ax.set_ylabel(f"{name}") + ax.axhline(0, color="gray", linestyle=":", alpha=0.3) + ax.grid(True, alpha=0.3) + ax.set_ylim(alpha[:n_plot, ci].min() - 0.5, alpha[:n_plot, ci].max() + 0.5) + axes[-1].set_xlabel("Step") + fig.suptitle(f"{scene_name} — Full Action Timeseries ({n_plot} steps)") + plt.tight_layout() + out = os.path.join(FIG_DIR, f"timeseries_full_{scene_name}.png") + plt.savefig(out, dpi=120) + plt.close() + print(f" Saved: {os.path.basename(out)}") + + +def check_fit_enforces_nobias(): + formulas_dir = os.path.join(os.path.dirname(__file__), "..", "validate", "results") + for scene_label in ["illusion_0.75L", "illusion_1L", "illusion_joint"]: + fpath = os.path.join(formulas_dir, f"pysr_{scene_label}_front.json") + if not os.path.exists(fpath): + # try results/formulas + fpath = os.path.join(os.path.dirname(__file__), "..", "results", "formulas", f"{scene_label}_front.json") + if not os.path.exists(fpath): + print(f"\n {scene_label}: formula file not found") + continue + formula = json.load(open(fpath)) + sym = formula.get("best_sympy", "") + score = formula.get("best_score", formula.get("r2_score", "?")) + keys = formula.get("feature_names", formula.get("feature_keys", [])) + print(f"\n {scene_label} front:") + print(f" formula: {sym}") + print(f" R2: {score}") + print(f" features ({len(keys)}): {keys[:8]}{'...' if len(keys)>8 else ''}") + + import re + consts = re.findall(r'[-+]?\s*\d+\.\d+', sym) + if consts: + print(f" standalone constants: {consts}") + if "bias" in sym.lower(): + print(f" WARNING: 'bias' found in front formula string!") + else: + print(f" ✓ No 'bias' keyword in formula") + + +def main(): + scenes = ["illusion_0.75L", "illusion_1L", "illusion_1.5L"] + + print("=" * 60) + print("DIAGNOSTIC 1: PPO Data Integrity") + print("=" * 60) + for sn in scenes: + try: + _, _, _, alpha, _ = load_scene(sn) + check_similarity_vs_training(sn) + check_actions(alpha, sn) + except Exception as e: + print(f"\n ERROR loading {sn}: {e}") + import traceback; traceback.print_exc() + + print("\n" + "=" * 60) + print("DIAGNOSTIC 2: G-Equivariance Check") + print("=" * 60) + for sn in scenes: + sensors, forces, _, alpha, _ = load_scene(sn) + check_gee_equivariance(alpha, sensors, forces, sn) + + print("\n" + "=" * 60) + print("DIAGNOSTIC 3: Phase Portraits + Timeseries") + print("=" * 60) + for sn in scenes: + sensors, _, _, alpha, _ = load_scene(sn) + plot_phase_portraits(sensors, sn) + plot_full_timeseries(alpha, sn) + + print("\n" + "=" * 60) + print("DIAGNOSTIC 4: Fit Pipeline Audit") + print("=" * 60) + check_fit_enforces_nobias() + + print(f"\nDone. Figures in {FIG_DIR}") + + +if __name__ == "__main__": + main() diff --git a/src/SR_analysis/scripts/gen_illusion_target.py b/src/SR_analysis/scripts/gen_illusion_target.py new file mode 100644 index 0000000..75bd759 --- /dev/null +++ b/src/SR_analysis/scripts/gen_illusion_target.py @@ -0,0 +1,167 @@ +"""Generate target data for Illusion scenes (no PPO model needed). + +Records target cylinder + sensor signals for any illusion scene. +Only Phase 1 of infer_illusion.py - does not need a PPO model. + +Usage: + conda run -n pycuda_3_10 python scripts/gen_illusion_target.py \\ + --scene illusion_0.6L --device 2 + + conda run -n pycuda_3_10 python scripts/gen_illusion_target.py \\ + --scene illusion_0.8L --device 2 + + conda run -n pycuda_3_10 python scripts/gen_illusion_target.py \\ + --scene all --device 2 +""" +from __future__ import annotations + +import argparse +import json +import os +import sys + +import numpy as np + +_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) +if _REPO not in sys.path: + sys.path.insert(0, _REPO) +_SRC = os.path.join(_REPO, "src") +if _SRC not in sys.path: + sys.path.insert(0, _SRC) + +from LegacyCelerisLab import FlowField +from SR_analysis.utils.cfd_interface import load_legacy_configs +from SR_analysis.configs import get_scene, get_scene_list, LEGACY_CFG_DIR, FIFO_LEN + +DATA_TYPE = np.float32 + + +def analyze_harmonics(states: np.ndarray, n_harmonics: int = 5) -> list: + """FFT-based harmonics analysis matching infer_illusion.py exactly.""" + N, D = states.shape + result = [] + for d in range(D): + y = states[:, d] + fft_coef = np.fft.rfft(y) + freqs = np.fft.rfftfreq(N, d=1) + amps = 2 * np.abs(fft_coef) / N + phases = np.angle(fft_coef) + idx = np.argsort(amps[1:])[::-1][:n_harmonics] + 1 + harmonics = { + "dc": float(np.real(fft_coef[0]) / N), + "amps": amps[idx].tolist(), + "freqs": freqs[idx].tolist(), + "phases": phases[idx].tolist(), + } + result.append(harmonics) + return result + + +def gen_target_for_scene(scene_name: str, device_id: int): + """Generate target data for one illusion scene (Phase 1 only).""" + cfg = get_scene(scene_name) + nu = cfg["nu"] + u0 = cfg["u0"] + l0 = 20.0 + sample_interval = cfg["sample_interval"] + sensor_x = cfg["sensor_x"] + target_diam = cfg["target_diameter"] + + output_root = os.path.join( + os.path.dirname(__file__), "..", "data", "illusion", scene_name, + ) + os.makedirs(output_root, exist_ok=True) + + print(f"\n{'='*60}") + print(f"Generating target for {scene_name} (diam={target_diam}L, SI={sample_interval})") + print(f"{'='*60}") + + cuda_cfg, field_cfg = load_legacy_configs(LEGACY_CFG_DIR) + field_cfg = field_cfg._replace(viscosity=float(nu)) + + ff = FlowField(field_cfg, cuda_cfg, device_id=device_id) + ny = ff.FIELD_SHAPE[1] + + # Add target cylinder at x=20*L0 + ff.add_cylinder((20.0 * l0, (ny - 1) / 2, 0.0), target_diam * l0) + + # Add 3 sensors at x = sensor_x * L0 + for y_off in [2.0, 0.0, -2.0]: + sc = (sensor_x * l0, (ny - 1) / 2 + y_off * l0, 0.0) + ff.add_sensor(sc, l0 / 4.0) + + n_obj = ff.obs.size // 2 + + # Stabilize + stabilize_steps = int(4 * ff.FIELD_SHAPE[0] / u0) + print(f" Stabilising ({stabilize_steps} steps)...") + ff.run(stabilize_steps, np.zeros(n_obj, dtype=DATA_TYPE)) + + # Record target: 8 channels = [cylinder_fx, fy, sensor0_ux,uy, sensor1_ux,uy, sensor2_ux,uy] + target_states = np.empty((0, 8), dtype=DATA_TYPE) + for _ in range(FIFO_LEN): + ff.run(sample_interval, np.zeros(n_obj, dtype=DATA_TYPE)) + new_state = ff.obs.copy()[0:8] + target_states = np.vstack((target_states, new_state)) + + print(f" Target recorded: {target_states.shape}") + + # Extract components + target_sensors = target_states[:, 2:8].copy() + target_forces = target_states[:, 0:2].copy() + target_harmonics = analyze_harmonics(target_forces, n_harmonics=5) + print(f" Harmonics computed: Cd_dc={target_harmonics[0]['dc']:.6f}, Cl_dc={target_harmonics[1]['dc']:.6f}") + print(f" Cd_amp_primary={target_harmonics[0]['amps'][0]:.6f}, Cl_amp_primary={target_harmonics[1]['amps'][0]:.6f}") + + # Save + np.savez(os.path.join(output_root, "target.npz"), + target_states=target_states, + target_sensors=target_sensors) + with open(os.path.join(output_root, "target_harmonics.json"), "w") as f: + json.dump(target_harmonics, f, indent=2) + + print(f" Saved to {output_root}") + + del ff + return True + + +def main(): + ap = argparse.ArgumentParser(description="Generate illusion target data") + ap.add_argument("--scene", type=str, default="all", + help='Scene name like "illusion_0.6L", or "all" for all generalization') + ap.add_argument("--device", type=int, default=2, help="GPU device") + args = ap.parse_args() + + if args.scene.lower() == "all": + # Only generate for generalization scenes + verify existing + all_scenes = ["illusion_0.5L", "illusion_0.6L", "illusion_0.8L", + "illusion_1.2L", "illusion_2L"] + to_gen = [] + for sn in all_scenes: + data_dir = os.path.join( + os.path.dirname(__file__), "..", "data", "illusion", sn) + if not os.path.isfile(os.path.join(data_dir, "target_harmonics.json")): + to_gen.append(sn) + else: + print(f" {sn}: target data already exists, skipping") + if not to_gen: + print("All target data already exists.") + return + scene_names = to_gen + else: + scene_names = [args.scene] + + for sn in scene_names: + try: + gen_target_for_scene(sn, args.device) + except Exception as e: + print(f"ERROR on {sn}: {e}") + import traceback + traceback.print_exc() + + print("\nDone.") + + +if __name__ == "__main__": + main() diff --git a/src/SR_analysis/scripts/test_cylinder_order.py b/src/SR_analysis/scripts/test_cylinder_order.py new file mode 100644 index 0000000..da84c09 --- /dev/null +++ b/src/SR_analysis/scripts/test_cylinder_order.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +"""A/B test: does Karman cylinder order in add_pinball() matter? + +Runs the same PPO checkpoint on two FlowFields with different cylinder addition +orders, then compares sensor signals and actions. +""" +import os, sys, json, time, numpy as np +from collections import deque + +_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) +sys.path.insert(0, _REPO) +_SRC = os.path.join(_REPO, "src"); sys.path.insert(0, _SRC) + +from LegacyCelerisLab import FlowField +from SR_analysis.utils.cfd_interface import load_legacy_configs, scale_action, build_karman_cloak_env, add_pinball as add_pinball_original, build_observation, load_ppo_model, compute_similarity +from SR_analysis.configs import get_scene, LEGACY_CFG_DIR, FIFO_LEN, CONV_LEN + +DATA_TYPE = np.float32 +SI = 800 + +def add_pinball_corrected(ff, **kw): + """Same as add_pinball but with bottom-first order matching training env.""" + import copy + from SR_analysis.utils.cfd_interface import add_pinball as _orig + # We can't easily modify add_pinball inline, so we'll do the steps manually + l0 = kw.get("l0", 20.0) + u0 = kw.get("u0", 0.01) + si_local = kw.get("sample_interval", SI) + fl = kw.get("fifo_len", FIFO_LEN) + dt = kw.get("data_type", DATA_TYPE) + ab = kw.get("action_bias", (0.0, -4.0, 4.0)) + pfx = kw.get("pinball_front_x", 30.0) + prx = kw.get("pinball_rear_x", 31.3) + oss = kw.get("obs_slice_start", 2) + ose = kw.get("obs_slice_end", 14) + + cy = (ff.FIELD_SHAPE[1] - 1) / 2.0 + u0_f = float(u0) + + # CORRECTED: front → BOTTOM(y=-0.75) → TOP(y=+0.75) + ff.add_cylinder((pfx * l0, cy, 0.0), l0 / 2.0) # front + ff.add_cylinder((prx * l0, cy - 0.75 * l0, 0.0), l0 / 2.0) # BOTTOM first + ff.add_cylinder((prx * l0, cy + 0.75 * l0, 0.0), l0 / 2.0) # TOP second + + n_obj = ff.obs.size // 2 + print(f" [corrected] bodies after pinball: {n_obj}") + + # stabilize + ff.run(int(4 * ff.FIELD_SHAPE[0] / u0_f), np.zeros(n_obj, dtype=dt)) + ff.get_ddf(); ff.save_ddf() + + # norm (zero-action) + fifo = deque(maxlen=fl) + for _ in range(fl): + ff.run(si_local, np.zeros(n_obj, dtype=dt)) + fifo.append(ff.obs.copy()[oss:ose]) + tsa = np.array(fifo, dtype=np.float32) + fnf = 6.0 * float(np.max(np.abs(tsa[:, 6:12]))) + sd = np.mean(tsa[:, 0:6], axis=0).astype(np.float32) + snf = np.zeros(6, dtype=np.float32) + for i in range(6): + snf[i] = 5.0 * float(np.max(np.abs(tsa[:, i] - sd[i]))) + + # bias FIFO + ff.apply_ddf() + bias = np.zeros(n_obj, dtype=dt) + # CORRECTED order: id=4=front(0), id=5=BOTTOM(-4), id=6=TOP(+4) + bias[n_obj - 3] = float(ab[0] * u0_f) # front + bias[n_obj - 2] = float(ab[1] * u0_f) # BOTTOM gets bias=-4 + bias[n_obj - 1] = float(ab[2] * u0_f) # TOP gets bias=+4 + print(f" [corrected] bias: front={bias[n_obj-3]:.6f}, bottom={bias[n_obj-2]:.6f}, top={bias[n_obj-1]:.6f}") + fifo.clear() + for _ in range(fl): + ff.run(si_local, bias); fifo.append(ff.obs.copy()[oss:ose]) + save_states = np.array(list(fifo), dtype=dt) + ff.get_ddf(); ff.save_ddf(); ff.apply_ddf() + + return {"force_norm_fact": fnf, "sens_deviation": sd.tolist(), + "sens_norm_fact": snf.tolist(), "action_bias": list(ab), + "save_states": save_states} + + +def run_inference(ff, norm, n_obj_total, action_scale, action_bias, u0, + target_states, model_path, s_dim, n_steps, label): + """Run controlled PPO inference and return sensors, actions, rewards.""" + model = load_ppo_model(model_path, device="cuda:2", s_dim=s_dim) + + # bias FIFO init + fifo = deque(maxlen=FIFO_LEN) + ba = scale_action(np.zeros(3, dtype=np.float32), + scale=action_scale, bias=action_bias, u0=u0, n_total_bodies=n_obj_total) + for _ in range(FIFO_LEN): + ff.context.push(); ff.run(SI, ba); ff.context.pop() + fifo.append(ff.obs.copy()[2:14]) + + s_c, f_c, a_c = [], [], [] + obs = np.zeros(12, dtype=np.float32) + ba_arr = np.array(list(ba), dtype=np.float32) + + for step in range(n_steps): + act, _ = model.predict(obs, deterministic=True) + act = act.astype(np.float32).flatten(); a_c.append(act.copy()) + ff.context.push() + ff.run(SI, scale_action(act, scale=action_scale, bias=action_bias, u0=u0, n_total_bodies=n_obj_total)) + ff.context.pop() + osl = ff.obs.copy()[2:14]; fifo.append(osl) + s_c.append(osl[0:6]); f_c.append(osl[6:12]) + forces_n = osl[6:12] / norm["force_norm_fact"] + sens_n = (osl[0:6] - norm["sens_deviation"]) / norm["sens_norm_fact"] + obs = np.clip(np.hstack([forces_n, sens_n]), -1, 1).astype(np.float32) + + sim = compute_similarity(target_states, np.array(s_c, dtype=np.float32), CONV_LEN) + return np.array(s_c), np.array(a_c), sim, fifo + + +def main(): + cfg = get_scene("karman_re100") + u0, nu, l0 = cfg["u0"], cfg["nu"], 20.0 + si, action_scale = cfg["sample_interval"], cfg["action_scale"] + action_bias = tuple(cfg["action_bias"]) + n_obj_total = cfg["n_objects_env"] + s_dim = cfg["s_dim"] + model_path = "models/old/d1a3o12_re100.zip" + n_steps = 100 # shorter for quick test + + cuda_cfg, field_cfg = load_legacy_configs(LEGACY_CFG_DIR) + field_cfg = field_cfg._replace(viscosity=float(nu)) + + print("=" * 60) + print("A/B TEST: Karman Cylinder Order") + print("=" * 60) + + # ── Run A: ORIGINAL order (front → TOP → BOTTOM) ── + print("\n--- A: ORIGINAL add_pinball (front→TOP→BOTTOM) ---") + ff = FlowField(field_cfg, cuda_cfg, device_id=2) + target_states, _ = build_karman_cloak_env(ff, u0=u0, l0=l0, sample_interval=si, fifo_len=FIFO_LEN, data_type=DATA_TYPE) + norm_a = add_pinball_original(ff, l0=l0, u0=u0, sample_interval=si, fifo_len=FIFO_LEN, data_type=DATA_TYPE, + action_bias=action_bias, pinball_front_x=cfg["pinball_front_x"], pinball_rear_x=cfg["pinball_rear_x"], + obs_slice_start=2, obs_slice_end=14) + ff.restore_ddf(); ff.apply_ddf() + # Show which cylinder gets which ID + print(f" Cylinder IDs: {list(ff.objects.keys())}") + for k, v in ff.objects.items(): + print(f" id={k}: center=({v['center'][0]:.0f}, {v['center'][1]:.1f}, {v['center'][2]:.0f})") + + sensors_a, actions_a, sim_a, _ = run_inference( + ff, norm_a, n_obj_total, action_scale, action_bias, u0, + target_states, model_path, s_dim, n_steps, "A") + del ff + print(f" A similarity: {sim_a:.4f}") + + # ── Run B: CORRECTED order (front → BOTTOM → TOP) ── + print("\n--- B: CORRECTED order (front→BOTTOM→TOP) ---") + ff = FlowField(field_cfg, cuda_cfg, device_id=2) + target_states2, _ = build_karman_cloak_env(ff, u0=u0, l0=l0, sample_interval=si, fifo_len=FIFO_LEN, data_type=DATA_TYPE) + norm_b = add_pinball_corrected(ff, l0=l0, u0=u0, sample_interval=si, fifo_len=FIFO_LEN, data_type=DATA_TYPE, + action_bias=action_bias, pinball_front_x=cfg["pinball_front_x"], pinball_rear_x=cfg["pinball_rear_x"], + obs_slice_start=2, obs_slice_end=14) + ff.restore_ddf(); ff.apply_ddf() + print(f" Cylinder IDs: {list(ff.objects.keys())}") + for k, v in ff.objects.items(): + print(f" id={k}: center=({v['center'][0]:.0f}, {v['center'][1]:.1f}, {v['center'][2]:.0f})") + + sensors_b, actions_b, sim_b, _ = run_inference( + ff, norm_b, n_obj_total, action_scale, action_bias, u0, + target_states2, model_path, s_dim, n_steps, "B") + del ff + print(f" B similarity: {sim_b:.4f}") + + # ── Compare ── + print("\n" + "=" * 60) + print("COMPARISON") + print("=" * 60) + print(f" Similarity: A={sim_a:.6f}, B={sim_b:.6f}, Δ={abs(sim_a - sim_b):.6f}") + + sensor_diff = np.max(np.abs(sensors_a - sensors_b)) + print(f" Max sensor diff (A-B): {sensor_diff:.8f}") + + # Channel-wise action comparison + for i, name in enumerate(["Front", "Bottom_channel", "Top_channel"]): + corr = np.corrcoef(actions_a[:, i], actions_b[:, i])[0, 1] + print(f" Action {name}: corr={corr:.6f}") + + # Physical action (omega) comparison + omega_a = (actions_a * action_scale + np.array(action_bias)) * u0 + omega_b = (actions_b * action_scale + np.array(action_bias)) * u0 + + print(f"\n Physical omega stats:") + for i, name in enumerate(["Front", "Bottom(phys ch1)", "Top(phys ch2)"]): + print(f" A {name}: μ={omega_a[:,i].mean():.6f}, σ={omega_a[:,i].std():.6f}") + print(f" B {name}: μ={omega_b[:,i].mean():.6f}, σ={omega_b[:,i].std():.6f}") + + # Check: is the "bottom channel" action in B actually controlling the bottom cylinder? + # If B correctly maps bias[-2]=bottom=cy-0.75 with bias=-4: + # bottom (cy-0.75): bias=-4, action from channel 1 + # top (cy+0.75): bias=+4, action from channel 2 + # If A incorrectly maps: + # top (cy+0.75): bias=-4, action from channel 1 + # bottom (cy-0.75): bias=+4, action from channel 2 + + print(f"\n VERDICT:") + if abs(sim_a - sim_b) < 0.01 and sensor_diff < 1e-4: + print(f" → NO DIFFERENCE. Cylinder order does not affect PPO output.") + print(f" → Either PPO's rear channels are identical, or LegacyCelerisLab remaps actions.") + elif sim_b > sim_a + 0.01: + print(f" → B is BETTER (corrected order improves similarity).") + print(f" → add_pinball order was indeed swapped and matters.") + else: + print(f" → Difference exists but not conclusive.") + + return 0 + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/SR_analysis/scripts/visualize_ppo_illusion.py b/src/SR_analysis/scripts/visualize_ppo_illusion.py new file mode 100644 index 0000000..05376ac --- /dev/null +++ b/src/SR_analysis/scripts/visualize_ppo_illusion.py @@ -0,0 +1,299 @@ +"""PPO inference with field visualization for Illusion scenes. + +Reuses infer_illusion.py's setup but adds vorticity field saving. +No SINDy involved — just PPO model in the loop. + +Usage: + conda run -n pycuda_3_10 python src/SR_analysis/scripts/visualize_ppo_illusion.py \\ + --diameter 1.0 --device 0 --steps 300 +""" +from __future__ import annotations + +import argparse +import json +import os +import sys +import time +from collections import deque + +import numpy as np + +_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) +if _REPO not in sys.path: + sys.path.insert(0, _REPO) +_SRC = os.path.join(_REPO, "src") +if _SRC not in sys.path: + sys.path.insert(0, _SRC) + +from LegacyCelerisLab import FlowField +from LegacyCelerisLab import utils as legacy_utils + +from SR_analysis.utils.cfd_interface import ( + load_legacy_configs, load_ppo_model, vorticity_from_ddf, save_vorticity_png, +) +from SR_analysis.configs import ( + get_scene, get_scene_list, model_path_for_scene, LEGACY_CFG_DIR, FIFO_LEN, +) + +DATA_TYPE = np.float32 +NX = 1280 + + +def analyze_harmonics(states: np.ndarray, n_harmonics: int = 5) -> list: + N, D = states.shape + result = [] + for d in range(D): + y = states[:, d] + fft_coef = np.fft.rfft(y) + freqs = np.fft.rfftfreq(N, d=1) + amps = 2 * np.abs(fft_coef) / N + phases = np.angle(fft_coef) + idx = np.argsort(amps[1:])[::-1][:n_harmonics] + 1 + harmonics = { + 'dc': float(np.real(fft_coef[0]) / N), + 'amps': amps[idx].tolist(), + 'freqs': freqs[idx].tolist(), + 'phases': phases[idx].tolist(), + } + result.append(harmonics) + return result + + +def gen_target_states_at(t: int, harmonics: list) -> np.ndarray: + D = len(harmonics) + result = np.zeros(D, dtype=np.float32) + for d, h in enumerate(harmonics): + val = h['dc'] + for amp, freq, phase in zip(h['amps'], h['freqs'], h['phases']): + val += amp * np.cos(2 * np.pi * freq * t + phase) + result[d] = float(val) + return result + + +def run_visualization(scene_name: str, device_id: int, n_infer_steps: int, + out_root: str): + cfg = get_scene(scene_name) + u0 = cfg["u0"] + l0 = 20.0 + sample_interval = cfg["sample_interval"] + action_scale = cfg["action_scale"] + action_bias = cfg["action_bias"] + target_diam = cfg["target_diameter"] + s_dim = cfg["s_dim"] + nu = cfg["nu"] + + # Auto-set steps if too short + min_steps = int(1.5 * NX / u0 / sample_interval) + if n_infer_steps < min_steps: + n_infer_steps = max(min_steps, 300) + print(f" auto-set steps={n_infer_steps} (min={min_steps})") + + os.makedirs(out_root, exist_ok=True) + + print(f"\n{'='*60}") + print(f"PPO viz: {scene_name} diam={target_diam}L steps={n_infer_steps}") + print(f"{'='*60}") + + # Load configs + cuda_cfg, field_cfg = load_legacy_configs(LEGACY_CFG_DIR) + field_cfg = field_cfg._replace(viscosity=float(nu)) + + # -- Phase 1: Target recording ------------------------------------------- + ff = FlowField(field_cfg, cuda_cfg, device_id=device_id) + ny = ff.FIELD_SHAPE[1] + + ff.add_cylinder((20.0 * l0, (ny - 1) / 2, 0.0), target_diam * l0) + for y_off in [2.0, 0.0, -2.0]: + sc = (30.0 * l0, (ny - 1) / 2 + y_off * l0, 0.0) + ff.add_sensor(sc, l0 / 4.0) + + stabilise = int(4 * NX / u0) + ff.run(stabilise, np.zeros(4, dtype=DATA_TYPE)) + + target_all = np.empty((0, 8), dtype=DATA_TYPE) + for _ in range(FIFO_LEN): + ff.run(sample_interval, np.zeros(4, dtype=DATA_TYPE)) + target_all = np.vstack((target_all, ff.obs.copy()[0:8])) + target_sensors = target_all[:, 2:8].copy() + target_forces = target_all[:, 0:2].copy() + target_harmonics = analyze_harmonics(target_forces) + + # Save target cylinder vorticity + omega_tgt = vorticity_from_ddf(ff, u0) + save_vorticity_png(os.path.join(out_root, f"{scene_name}_target_vorticity.png"), + omega_tgt, title=f"{scene_name} target ({target_diam}L)") + print(f" target vorticity saved") + + del ff + + # -- Phase 2: Pinball env setup ----------------------------------------- + ff = FlowField(field_cfg, cuda_cfg, device_id=device_id) + + for y_off in [2.0, 0.0, -2.0]: + sc = (30.0 * l0, (ny - 1) / 2 + y_off * l0, 0.0) + ff.add_sensor(sc, l0 / 4.0) + ff.add_cylinder((19.0 * l0, (ny - 1) / 2, 0.0), l0 / 2.0) + ff.add_cylinder((20.3 * l0, (ny - 1) / 2 + 0.75 * l0, 0.0), l0 / 2.0) + ff.add_cylinder((20.3 * l0, (ny - 1) / 2 - 0.75 * l0, 0.0), l0 / 2.0) + + n_obj = ff.obs.size // 2 + assert n_obj == 6 + ff.run(stabilise, np.zeros(n_obj, dtype=DATA_TYPE)) + + # save_ddf(1): post-stabilization + ff.get_ddf() + ff.save_ddf() + + # Norm collection + fifo = deque(maxlen=FIFO_LEN) + for _ in range(FIFO_LEN): + ff.run(sample_interval, np.zeros(n_obj, dtype=DATA_TYPE)) + fifo.append(ff.obs.copy()[0:12]) + temp = np.array(fifo, dtype=DATA_TYPE) + force_norm_fact = 6.0 * float(np.max(np.abs(temp[:, 6:12]))) + sens_deviation = np.mean(temp[:, 0:6], axis=0).astype(DATA_TYPE) + sens_norm_fact = np.zeros(6, dtype=DATA_TYPE) + for i in range(6): + sens_norm_fact[i] = 5.0 * float(np.max(np.abs(temp[:, i] - sens_deviation[i]))) + print(f" norm: force_norm_fact={force_norm_fact:.6f}") + + # Bias FIFO rollout: [0, -U0, U0] + ff.apply_ddf() + bias_arr = np.zeros(n_obj, dtype=DATA_TYPE) + bias_arr[3] = 0.0 + bias_arr[4] = -1.0 * u0 + bias_arr[5] = 1.0 * u0 + + fifo.clear() + for _ in range(FIFO_LEN): + ff.run(sample_interval, bias_arr) + fifo.append(ff.obs.copy()[0:12]) + + # save_ddf(2): post-bias + ff.get_ddf() + ff.save_ddf() + ff.apply_ddf() + + # Save uncontrolled reference (before PPO, same initial state, same duration) + ff_un = FlowField(field_cfg, cuda_cfg, device_id=device_id) + ff_un.add_sensor((30.0 * l0, (ny - 1) / 2 + 2.0 * l0, 0.0), l0 / 4.0) + ff_un.add_sensor((30.0 * l0, (ny - 1) / 2, 0.0), l0 / 4.0) + ff_un.add_sensor((30.0 * l0, (ny - 1) / 2 - 2.0 * l0, 0.0), l0 / 4.0) + ff_un.add_cylinder((19.0 * l0, (ny - 1) / 2, 0.0), l0 / 2.0) + ff_un.add_cylinder((20.3 * l0, (ny - 1) / 2 + 0.75 * l0, 0.0), l0 / 2.0) + ff_un.add_cylinder((20.3 * l0, (ny - 1) / 2 - 0.75 * l0, 0.0), l0 / 2.0) + ff_un.run(stabilise, np.zeros(6, dtype=DATA_TYPE)) + ff_un.get_ddf() + ff_un.save_ddf() + ff_un.apply_ddf() + for _ in range(FIFO_LEN): + ff_un.run(sample_interval, bias_arr) + ff_un.get_ddf() + ff_un.save_ddf() + ff_un.apply_ddf() + for _ in range(n_infer_steps): + ff_un.run(sample_interval, np.zeros(6, dtype=DATA_TYPE)) + omega_unc = vorticity_from_ddf(ff_un, u0) + save_vorticity_png(os.path.join(out_root, f"{scene_name}_uncontrolled_vorticity.png"), + omega_unc, title=f"{scene_name} uncontrolled (zero action)") + print(f" uncontrolled vorticity saved") + del ff_un + + # -- Phase 3: PPO controlled rollout ------------------------------------ + model_path = model_path_for_scene(scene_name) + if model_path is None: + print(f" ERROR: no model for {scene_name}") + return + + model = load_ppo_model(model_path, device=f"cuda:{device_id}", s_dim=s_dim) + model.set_random_seed(0) + + print(f" PPO controlled rollout ({n_infer_steps} steps) ...") + ff.restore_ddf() + ff.apply_ddf() + + # FIFO reinit: run bias for FIFO_LEN steps + fifo = deque(maxlen=FIFO_LEN) + for _ in range(FIFO_LEN): + ff.context.push() + ff.run(sample_interval, bias_arr) + ff.context.pop() + fifo.append(ff.obs.copy()[0:12]) + + obs = np.zeros(s_dim, dtype=np.float32) + field_save_interval = max(1, n_infer_steps // 8) + + for step in range(n_infer_steps): + action, _ = model.predict(obs, deterministic=True) + action = action.astype(np.float32).flatten() + + temp = np.zeros(n_obj, dtype=DATA_TYPE) + temp[3:6] = np.array( + (action * action_scale + list(action_bias)) * u0, dtype=DATA_TYPE) + + ff.context.push() + ff.run(sample_interval, temp) + ff.context.pop() + + obs_slice = ff.obs.copy()[0:12] + fifo.append(obs_slice) + + forces_norm = obs_slice[6:12] / force_norm_fact + sens_norm = (obs_slice[0:6] - sens_deviation) / sens_norm_fact + obs12 = np.clip(np.hstack([forces_norm, sens_norm]), -1, 1).astype(np.float32) + + if s_dim == 14: + tv = gen_target_states_at(step, target_harmonics) + target_cd = np.clip(tv[0] / force_norm_fact, -1, 1) + target_cl = np.clip(tv[1] / force_norm_fact, -1, 1) + obs = np.zeros(14, dtype=np.float32) + obs[:12] = obs12 + obs[12] = target_cd + obs[13] = target_cl + else: + obs = obs12 + + # Save field at intervals + if step % field_save_interval == 0 or step == n_infer_steps - 1: + omega = vorticity_from_ddf(ff, u0) + pct = int(100 * step / n_infer_steps) + fname = os.path.join(out_root, f"{scene_name}_PPO_step{step:04d}_pct{pct:02d}.png") + save_vorticity_png(fname, omega, + title=f"{scene_name} PPO step {step}/{n_infer_steps} ({pct}%)") + print(f" field saved: step {step}/{n_infer_steps} ({pct}%)") + + # Final field + omega = vorticity_from_ddf(ff, u0) + save_vorticity_png(os.path.join(out_root, f"{scene_name}_PPO_vorticity.png"), + omega, title=f"{scene_name} PPO controlled (sim 0.97+)") + print(f" final vorticity saved") + + del ff + print(f" Done: {scene_name}") + + +def main(): + ap = argparse.ArgumentParser(description="PPO visualization for Illusion") + ap.add_argument("--diameter", type=str, default="1.0", help='0.75, 1.0, 1.5, or all') + ap.add_argument("--device", type=int, default=0) + ap.add_argument("--steps", type=int, default=300) + ap.add_argument("--out-root", type=str, default=None) + args = ap.parse_args() + + if args.diameter.lower() == "all": + scene_names = get_scene_list("illusion") + else: + d = float(args.diameter) + scene_names = [sn for sn in get_scene_list("illusion") + if abs(get_scene(sn)["target_diameter"] - d) < 0.01] + + if args.out_root is None: + args.out_root = os.path.join(os.path.dirname(__file__), "..", + "validate", "results") + + for sn in scene_names: + run_visualization(sn, args.device, args.steps, args.out_root) + + +if __name__ == "__main__": + main() diff --git a/src/SR_analysis/sindy/run_pysr_deep.py b/src/SR_analysis/sindy/run_pysr_deep.py new file mode 100644 index 0000000..ebabf07 --- /dev/null +++ b/src/SR_analysis/sindy/run_pysr_deep.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +"""Deep PySR search for Karman cross-Re: individual + joint, niterations=120. + +Usage: + conda run -n sr_env python src/SR_analysis/sindy/run_pysr_deep.py --individual + conda run -n sr_env python src/SR_analysis/sindy/run_pysr_deep.py --joint + conda run -n sr_env python src/SR_analysis/sindy/run_pysr_deep.py --both +""" +from __future__ import annotations +import json, os, sys +import numpy as np +from pysr import PySRRegressor + +_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) +sys.path.insert(0, _REPO); sys.path.insert(0, os.path.join(_REPO, "src")) +from SR_analysis.utils.feature_builder import ( + compute_dimensionless, compute_features, build_feature_matrix, + CORE_FEAT_KEYS_V2, PHASE_STATE_KEYS, MU_FEAT_KEYS, +) +from SR_analysis.configs import get_scene + +SINDY_DIR = os.path.join(os.path.dirname(__file__)) +ALL_MU = list(MU_FEAT_KEYS) + ["mu_Cl_tot"] +RESULTS_DIR = os.path.join(SINDY_DIR, "..", "validate", "results") +os.makedirs(RESULTS_DIR, exist_ok=True) +RE_SCENES = ["karman_re50", "karman_re100", "karman_re200", "karman_re400"] + +# Feature sets +PHYS = [k for k in CORE_FEAT_KEYS_V2 if not k.startswith(("aF_","aB_","aT_","daF","daB","daT"))] +PHYS_DADT = PHYS + ["daF_dt","daB_dt","daT_dt"] + +DESC = { + "phase": ("PHASE_STATE_KEYS (6)", PHASE_STATE_KEYS), + "phys": ("physics only (8)", PHYS), + "phys_dadt": ("phys + da/dt (11)", PHYS_DADT), + "full": ("CORE_FEAT_V2 + mu (20)", CORE_FEAT_KEYS_V2 + ALL_MU), +} + +def load_and_build(scene_name, front_keys, use_mu=True): + cfg = get_scene(scene_name); u0=cfg["u0"]; mu=cfg["mu"]; dc=cfg.get("sample_interval",800)/2000.0 + npz=np.load(f"{SINDY_DIR}/../data/karman/{scene_name}/controlled.npz") + s=npz["sensors"].astype(np.float64); f=npz["forces"].astype(np.float64) + ap=(npz["actions"].astype(np.float64)*cfg["action_scale"]+np.array(cfg["action_bias"]))*u0 + T=s.shape[0]; a1=np.zeros((T,3)); a2=np.zeros((T,3)) + a1[1:]=ap[:-1]; a2[2:]=ap[:-2] + dim=compute_dimensionless(s,f,u0=u0,d=20.0) + sym=compute_features(dim,a1,a2,mu,alpha_mode=False,include_mu=use_mu,include_cos_sin=False,u0=u0,dt_c=dc,sensors_raw=s,forces_raw=f) + Xf=build_feature_matrix(sym,front_keys,add_bias=False)[2:]; Ya=ap[2:]/u0 + return Xf, Ya + +def run_pysr(X, y, feat_names, label, seed=42, niter=120): + print(f"\n--- {label} ({X.shape[0]} samples, {X.shape[1]} feats) ---") + for i,k in enumerate(feat_names): + print(f" {k}: std={np.std(X[:,i]):.6f}") + m = PySRRegressor( + binary_operators=["+","-","*","/"], unary_operators=["square"], + niterations=niter, populations=40, maxsize=15, + complexity_of_constants=2, parsimony=0.01, batching=False, + random_state=seed, + ) + m.fit(X, y, variable_names=feat_names) + return { + "best_sympy": str(m.sympy()), + "best_score": float(m.score(X, y)), + "equations": m.equations_.to_dict(orient="records"), + } + +def run_individual(run_id=""): + """Run deep PySR on each Re independently with phys_dadt features.""" + print("="*60) + print("INDIVIDUAL Re (each with phys_dadt + mu)") + print("="*60) + front_keys = PHYS_DADT + ALL_MU + all_res = {} + for sn in RE_SCENES: + Xf, Ya = load_and_build(sn, front_keys, use_mu=True) + res = run_pysr(Xf, Ya[:, 0], front_keys, f"{sn} front", niter=120) + all_res[sn] = {"front": res} + out = os.path.join(RESULTS_DIR, f"pysr_deep_individual{run_id}.json") + json.dump(all_res, open(out, "w"), indent=2, default=str) + for sn in RE_SCENES: + r = all_res[sn]["front"] + print(f" {sn}: {r['best_sympy']} (R2={r['best_score']:.4f})") + +def run_joint(run_id=""): + """Run deep joint PySR across all Re.""" + print("="*60) + print("JOINT (phys_dadt + mu, across all Re)") + print("="*60) + front_keys = PHYS_DADT + ALL_MU + X_list, Y_list = [], [] + for sn in RE_SCENES: + Xf, Ya = load_and_build(sn, front_keys, use_mu=True) + X_list.append(Xf); Y_list.append(Ya[:, 0]) + X_all = np.vstack(X_list); Y_all = np.concatenate(Y_list) + res = run_pysr(X_all, Y_all, front_keys, "joint front", niter=120) + out = os.path.join(RESULTS_DIR, f"pysr_deep_joint{run_id}.json") + json.dump(res, open(out, "w"), indent=2, default=str) + print(f"\nJoint: {res['best_sympy']} (R2={res['best_score']:.4f})") + +def show_pareto(path, label): + d = json.load(open(path)) + if "front" in d: + eqs = d["front"]["equations"] + elif "equations" in d: + eqs = d["equations"] + else: + print(f" No equations in {path}") + return + print(f"\n=== Pareto {label} ===") + for eq in eqs[:15]: + print(f" c={eq['complexity']:2d} loss={eq['loss']:.6f} {eq['equation']}") + +if __name__ == "__main__": + import argparse + ap = argparse.ArgumentParser() + ap.add_argument("--individual", action="store_true") + ap.add_argument("--joint", action="store_true") + ap.add_argument("--both", action="store_true") + ap.add_argument("--show", type=str, default=None, help="Show pareto for saved result") + args = ap.parse_args() + if args.show: + show_pareto(args.show, args.show) + elif args.both: + run_individual("_deep"); run_joint("_deep") + elif args.individual: + run_individual("_deep") + elif args.joint: + run_joint("_deep") + else: + run_individual("_deep"); run_joint("_deep") diff --git a/src/SR_analysis/sindy/run_pysr_deep_illusion.py b/src/SR_analysis/sindy/run_pysr_deep_illusion.py new file mode 100644 index 0000000..899e856 --- /dev/null +++ b/src/SR_analysis/sindy/run_pysr_deep_illusion.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python3 +"""Deep PySR search for Illusion: individual + joint, niterations=120. + +Adapted from run_pysr_deep.py for illusion scenes: +- Uses ILLUSION_PHASE_KEYS (10-dim) + target_Cd/target_Cl (2-dim) +- Data loaded from illusion directories +- Supports both individual (per-scene) and joint (cross-diameter) search + +Usage: + conda run -n sr_env python src/SR_analysis/sindy/run_pysr_deep_illusion.py --individual + conda run -n sr_env python src/SR_analysis/sindy/run_pysr_deep_illusion.py --joint + conda run -n sr_env python src/SR_analysis/sindy/run_pysr_deep_illusion.py --both +""" +from __future__ import annotations +import json, os, sys +import numpy as np +from pysr import PySRRegressor + +_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) +sys.path.insert(0, _REPO); sys.path.insert(0, os.path.join(_REPO, "src")) +from SR_analysis.utils.feature_builder import ( + compute_dimensionless, compute_features, build_feature_matrix, + ILLUSION_PHASE_KEYS, +) +from SR_analysis.configs import get_scene + +SINDY_DIR = os.path.join(os.path.dirname(__file__)) +RESULTS_DIR = os.path.join(SINDY_DIR, "..", "validate", "results") +os.makedirs(RESULTS_DIR, exist_ok=True) + +# Illusion scenes (skip 1.5L - bang-bang regime) +ILLUSION_SCENES = ["illusion_0.75L", "illusion_1L"] + +# Illusion phase keys + target force features (for front channel) +# ILLUSION_PHASE_KEYS has 10 dims: u_a, du_a_dt, Cl_tot, dCl_tot_dt, Cd_tot, Cd_rear, +# Cd_err, Cl_err, dCd_err_dt, dCl_err_dt +# For joint search, also include target features (already part of ILLUSION_PHASE_KEYS +# via Cd_err = Cd_tot - target_Cd, etc.) +FRONT_KEYS = list(ILLUSION_PHASE_KEYS) +REAR_KEYS = list(ILLUSION_PHASE_KEYS) # same features, bias added by build_feature_matrix + +# Extended feature set with explicit target_Cd/target_Cl +FRONT_KEYS_WITH_TARGET = FRONT_KEYS + ["target_Cd", "target_Cl"] +REAR_KEYS_WITH_TARGET = REAR_KEYS + ["target_Cd", "target_Cl"] + + +def load_and_build(scene_name, front_keys, rear_keys, use_mu=False, dt_c=None): + """Load controlled data and build feature matrix for one illusion scene.""" + cfg = get_scene(scene_name) + u0 = cfg["u0"] + mu = cfg["mu"] + si = cfg.get("sample_interval", 600) + t0_steps = 2000 + if dt_c is None: + dt_c = si / t0_steps + + data_dir = os.path.join(SINDY_DIR, "..", "data", "illusion", scene_name) + npz = np.load(os.path.join(data_dir, "controlled.npz")) + sensors = npz["sensors"].astype(np.float64) + forces = npz["forces"].astype(np.float64) + actions_norm = npz["actions"].astype(np.float64) + actions_phys = (actions_norm * cfg["action_scale"] + np.array(cfg["action_bias"])) * u0 + target_forces = npz["target_forces"].astype(np.float64) if "target_forces" in npz else None + + T = sensors.shape[0] + a1 = np.zeros((T, 3), dtype=np.float64) + a2 = np.zeros((T, 3), dtype=np.float64) + a1[1:] = actions_phys[:-1] + a2[2:] = actions_phys[:-2] + + dim = compute_dimensionless(sensors, forces, u0=u0, d=20.0) + sym = compute_features( + dim, a1, a2, mu, + alpha_mode=False, include_mu=use_mu, + include_cos_sin=False, dt_c=dt_c, u0=u0, + target_forces=target_forces, + sensors_raw=sensors, forces_raw=forces, + ) + + n_warmup = 2 + Xf = build_feature_matrix(sym, front_keys, add_bias=False)[n_warmup:] + Xr = build_feature_matrix(sym, rear_keys, add_bias=True)[n_warmup:] + Ya = actions_phys[n_warmup:] / u0 # non-dim alpha + return Xf, Xr, Ya, {"u0": u0, "mu": mu, "cfg": cfg, "scene_name": scene_name, + "sensors": sensors, "forces": forces, "target_forces": target_forces, + "dt_c": dt_c} + + +def run_pysr(X, y, feat_names, label, seed=42, niter=120): + """Run PySR on feature matrix X -> target y.""" + print(f"\n--- {label} ({X.shape[0]} samples, {X.shape[1]} feats) ---") + for i, k in enumerate(feat_names): + print(f" {k}: std={np.std(X[:, i]):.6f}") + + m = PySRRegressor( + binary_operators=["+", "-", "*", "/"], + unary_operators=["square"], + niterations=niter, + populations=40, + maxsize=15, + complexity_of_constants=2, + parsimony=0.01, + batching=False, + random_state=seed, + ) + m.fit(X, y, variable_names=feat_names) + return { + "best_sympy": str(m.sympy()), + "best_score": float(m.score(X, y)), + "equations": m.equations_.to_dict(orient="records"), + } + + +def run_individual(run_id="", with_target=False): + """Run deep PySR on each illusion scene independently.""" + print("=" * 60) + print("INDIVIDUAL Illusion (each with ILLUSION_PHASE_KEYS + target_Cd/target_Cl)") + print("=" * 60) + + front_keys = FRONT_KEYS_WITH_TARGET if with_target else FRONT_KEYS + rear_keys = REAR_KEYS_WITH_TARGET if with_target else REAR_KEYS + + all_res = {} + for sn in ILLUSION_SCENES: + print(f"\n--- {sn} ---") + Xf, Xr, Ya, info = load_and_build(sn, front_keys, rear_keys) + print(f" Data loaded: Xf={Xf.shape}, Ya={Ya.shape}") + + # Front channel + res_f = run_pysr(Xf, Ya[:, 0], front_keys, f"{sn} front", niter=120) + # Top channel (with bias) + res_t = run_pysr(Xr, Ya[:, 2], ["bias"] + rear_keys, f"{sn} top", niter=120) + + all_res[sn] = {"front": res_f, "top": res_t} + + out_path = os.path.join(RESULTS_DIR, f"pysr_illusion_deep_individual{run_id}.json") + json.dump(all_res, open(out_path, "w"), indent=2, default=str) + print(f"\nSaved: {out_path}") + + for sn in ILLUSION_SCENES: + r = all_res[sn] + print(f" {sn}:") + print(f" front: {r['front']['best_sympy']} (R2={r['front']['best_score']:.4f})") + print(f" top: {r['top']['best_sympy']} (R2={r['top']['best_score']:.4f})") + + +def run_joint(run_id=""): + """Run deep joint PySR across both Illusion scenes. + + Method B: no target_diameter marker. Just concatenated data. + """ + print("=" * 60) + print("JOINT Illusion (ILLUSION_PHASE_KEYS, both scenes)") + print("=" * 60) + + Xf_list, Xr_list, Y_list = [], [], [] + for sn in ILLUSION_SCENES: + Xf, Xr, Ya, info = load_and_build(sn, FRONT_KEYS, REAR_KEYS) + Xf_list.append(Xf) + Xr_list.append(Xr) + Y_list.append(Ya) + + Xf_all = np.vstack(Xf_list) + Xr_all = np.vstack(Xr_list) + Y_all = np.vstack(Y_list) + print(f" Joint front: {Xf_all.shape}, rear: {Xr_all.shape}") + + res_f = run_pysr(Xf_all, Y_all[:, 0], FRONT_KEYS, "joint front", niter=120) + res_t = run_pysr(Xr_all, Y_all[:, 2], ["bias"] + REAR_KEYS, "joint top", niter=120) + + out_path = os.path.join(RESULTS_DIR, f"pysr_illusion_joint{run_id}.json") + json.dump({"front": res_f, "top": res_t}, open(out_path, "w"), indent=2, default=str) + print(f"\nSaved: {out_path}") + print(f" joint front: {res_f['best_sympy']} (R2={res_f['best_score']:.4f})") + print(f" joint top: {res_t['best_sympy']} (R2={res_t['best_score']:.4f})") + + +def run_joint_marker(run_id=""): + """Joint with target_diameter as extra feature (Method A). + + Concatenate data, add target_diameter column to feature matrix. + If the optimal formula uses target_diameter, formulas are different-shaped. + """ + print("=" * 60) + print("JOINT Illusion with target_diameter marker (Method A)") + print("=" * 60) + + Xf_list, Xr_list, Y_list = [], [], [] + diam_map = {"illusion_0.75L": 0.75, "illusion_1L": 1.0} + + for sn in ILLUSION_SCENES: + Xf, Xr, Ya, info = load_and_build(sn, FRONT_KEYS, REAR_KEYS) + diam = diam_map[sn] + # Add target_diameter column to both front and rear feature matrices + diam_col = np.full((Xf.shape[0], 1), diam, dtype=np.float64) + Xf_d = np.hstack([Xf, diam_col]) + Xr_d = np.hstack([Xr, diam_col]) + Xf_list.append(Xf_d) + Xr_list.append(Xr_d) + Y_list.append(Ya) + + Xf_all = np.vstack(Xf_list) + Xr_all = np.vstack(Xr_list) + Y_all = np.vstack(Y_list) + + marker_keys = FRONT_KEYS + ["target_diameter"] + marker_keys_r = ["bias"] + REAR_KEYS + ["target_diameter"] + print(f" Joint marker front: {Xf_all.shape}, rear: {Xr_all.shape}") + print(f" Front keys: {marker_keys}") + + res_f = run_pysr(Xf_all, Y_all[:, 0], marker_keys, "joint marker front", niter=120) + res_t = run_pysr(Xr_all, Y_all[:, 2], marker_keys_r, "joint marker top", niter=120) + + out_path = os.path.join(RESULTS_DIR, f"pysr_illusion_joint_marker{run_id}.json") + json.dump({"front": res_f, "top": res_t}, open(out_path, "w"), indent=2, default=str) + print(f"\nSaved: {out_path}") + print(f" joint marker front: {res_f['best_sympy']} (R2={res_f['best_score']:.4f})") + print(f" joint marker top: {res_t['best_sympy']} (R2={res_t['best_score']:.4f})") + + # Check if target_diameter is used in the best formula + if "target_diameter" in res_f.get("best_sympy", "") or "target_diameter" in res_t.get("best_sympy", ""): + print("\n >>> target_diameter IS used: formulas are DIFFERENT-shaped") + else: + print("\n >>> target_diameter NOT used: formulas may share a skeleton") + + +def show_pareto(path, label): + """Display Pareto front from a saved result.""" + d = json.load(open(path)) + if "front" in d and isinstance(d["front"], dict) and "equations" in d["front"]: + eqs = d["front"]["equations"] + elif "equations" in d: + eqs = d["equations"] + else: + print(f" No equations in {path}") + return + print(f"\n=== Pareto {label} ===") + for eq in eqs[:15]: + print(f" c={eq['complexity']:2d} loss={eq['loss']:.6f} {eq['equation']}") + + +if __name__ == "__main__": + import argparse + ap = argparse.ArgumentParser() + ap.add_argument("--individual", action="store_true") + ap.add_argument("--joint", action="store_true") + ap.add_argument("--joint-marker", action="store_true") + ap.add_argument("--with-target", action="store_true", + help="Include target_Cd/target_Cl as explicit features (12-dim total)") + ap.add_argument("--both", action="store_true") + ap.add_argument("--show", type=str, default=None, help="Show pareto for saved result") + args = ap.parse_args() + + if args.show: + show_pareto(args.show, args.show) + elif args.both: + run_individual("_deep", with_target=args.with_target) + run_joint("_deep") + run_joint_marker("_deep") + elif args.individual: + run_individual("_deep", with_target=args.with_target) + elif args.joint: + run_joint("_deep") + elif args.joint_marker: + run_joint_marker("_deep") + else: + run_individual("_deep") + run_joint("_deep") + run_joint_marker("_deep") diff --git a/src/SR_analysis/stage_1_infer.py b/src/SR_analysis/stage_1_infer.py new file mode 100644 index 0000000..696dce2 --- /dev/null +++ b/src/SR_analysis/stage_1_infer.py @@ -0,0 +1,451 @@ +#!/usr/bin/env python3 +"""Stage 1: Unified PPO inference pipeline for all scenes. + +Drives LegacyCelerisLab CFD + PPO model to produce controlled.npz and +target.npz for Karman cloak, Illusion, and Vortex scenes. + +Usage: + # Single scene + conda run -n pycuda_3_10 python stage_1_infer.py --scene karman_re100 --device 2 + + # Scene group + conda run -n pycuda_3_10 python stage_1_infer.py --group illusion_trained --device 2 + + # Target only (no PPO model needed, for generalization scenes) + conda run -n pycuda_3_10 python stage_1_infer.py --scene illusion_0.6L --target-only --device 2 + + # Dry run (verify configs and model paths) + conda run -n pycuda_3_10 python stage_1_infer.py --scene karman_re100 --dry-run + +Output per scene in data/{scene_id}/{scene_name}/: + target.npz — sensor signals from target phase + controlled.npz — sensors, forces, actions (normalized [-1,1]) from controlled phase + uncontrolled.npz — (Karman only) sensors, forces from zero-action + config.json — scene parameters + norm.json — normalization factors + result.json — similarity + reward summary + target_harmonics.json — (Illusion only) FFT harmonics for target force reconstruction +""" +from __future__ import annotations + +import argparse +import json +import os +import sys +import time +from collections import deque +from pathlib import Path + +import numpy as np + +_REPO = Path(__file__).resolve().parents[1] +if str(_REPO) not in sys.path: + sys.path.insert(0, str(_REPO)) +_SRC = _REPO / "src" +if str(_SRC) not in sys.path: + sys.path.insert(0, str(_SRC)) + +from LegacyCelerisLab import FlowField + +from SR_analysis.utils.cfd_interface import ( + nu_from_re, load_legacy_configs, build_karman_cloak_env, add_pinball, + build_observation, scale_action, load_ppo_model, save_vorticity_png, + vorticity_from_ddf, compute_similarity, +) +from SR_analysis.configs import ( + SCENES, get_scene, get_scene_list, model_path_for_scene, + LEGACY_CFG_DIR, FIFO_LEN, CONV_LEN, +) + +DATA_TYPE = np.float32 + +# ── Scene groups ────────────────────────────────────────────────────── +SCENE_GROUPS = { + "karman_all": lambda: get_scene_list("karman"), + "karman_trained": lambda: [s for s in get_scene_list("karman") if "re25" not in s and "re70" not in s and "re150" not in s and "re300" not in s], + "illusion_trained": lambda: ["illusion_0.75L", "illusion_1L", "illusion_1.5L"], + "illusion_generalization": lambda: ["illusion_0.5L", "illusion_0.6L", "illusion_0.8L", "illusion_1.2L", "illusion_2L"], + "vortex_all": lambda: get_scene_list("vortex"), +} + + +def _out_dir(scene_name: str) -> str: + cfg = get_scene(scene_name) + return os.path.join("src", "SR_analysis", "data", cfg["scene_id"], scene_name) + + +# ── Karman inference ────────────────────────────────────────────────── +def infer_karman(scene_name: str, device_id: int, out_dir: str, n_steps: int) -> dict: + cfg = get_scene(scene_name) + nu, u0, l0 = cfg["nu"], cfg["u0"], 20.0 + si, action_scale, action_bias = cfg["sample_interval"], cfg["action_scale"], cfg["action_bias"] + n_obj_total = cfg["n_objects_env"] + + os.makedirs(out_dir, exist_ok=True) + json.dump({k: v for k, v in cfg.items() if k != "action_bias"}, open(os.path.join(out_dir, "config.json"), "w"), indent=2, default=str) + + cuda_cfg, field_cfg = load_legacy_configs(LEGACY_CFG_DIR) + field_cfg = field_cfg._replace(viscosity=float(nu)) + + ff = FlowField(field_cfg, cuda_cfg, device_id=device_id) + target_states, _ = build_karman_cloak_env(ff, u0=u0, l0=l0, sample_interval=si, fifo_len=FIFO_LEN, data_type=DATA_TYPE) + np.savez(os.path.join(out_dir, "target.npz"), target_states=target_states) + + norm = add_pinball(ff, l0=l0, u0=u0, sample_interval=si, fifo_len=FIFO_LEN, data_type=DATA_TYPE, + action_bias=action_bias, pinball_front_x=cfg["pinball_front_x"], pinball_rear_x=cfg["pinball_rear_x"], + obs_slice_start=cfg["obs_slice"][0], obs_slice_end=cfg["obs_slice"][1]) + json.dump({k: v for k, v in norm.items() if not isinstance(v, np.ndarray)}, open(os.path.join(out_dir, "norm.json"), "w"), indent=2, default=str) + + # Uncontrolled + ff.restore_ddf(); ff.apply_ddf() + sens_u, forc_u = [], [] + for _ in range(n_steps): + ff.run(si, np.zeros(n_obj_total, dtype=DATA_TYPE)) + o = ff.obs.copy()[2:14]; sens_u.append(o[0:6]); forc_u.append(o[6:12]) + np.savez(os.path.join(out_dir, "uncontrolled.npz"), sensors=np.array(sens_u, dtype=np.float32), forces=np.array(forc_u, dtype=np.float32)) + + # Controlled + result = {"scene": scene_name, "controlled": False} + model_path = model_path_for_scene(scene_name) + if model_path: + model = load_ppo_model(model_path, device=f"cuda:{device_id}", s_dim=cfg.get("s_dim", 12)) + ff.restore_ddf(); ff.apply_ddf() + fifo = deque(maxlen=FIFO_LEN) + ba = scale_action(np.zeros(3, dtype=np.float32), scale=action_scale, bias=action_bias, u0=u0, n_total_bodies=n_obj_total) + for _ in range(FIFO_LEN): + ff.context.push(); ff.run(si, ba); ff.context.pop() + fifo.append(ff.obs.copy()[2:14]) + + s_c, f_c, a_c, r_c = [], [], [], [] + obs = np.zeros(12, dtype=np.float32) + for _ in range(n_steps): + act, _ = model.predict(obs, deterministic=True) + act = act.astype(np.float32).flatten(); a_c.append(act.copy()) + ff.context.push(); ff.run(si, scale_action(act, scale=action_scale, bias=action_bias, u0=u0, n_total_bodies=n_obj_total)); ff.context.pop() + osl = ff.obs.copy()[2:14]; fifo.append(osl) + s_c.append(osl[0:6]); f_c.append(osl[6:12]); obs = build_observation(osl, norm) + if len(fifo) >= CONV_LEN: + sa = np.array(list(fifo), dtype=np.float32) + cd = float((sa[-1,0] + sa[-1,2] + sa[-1,4]) / 3 / norm["force_norm_fact"]) + cl = float((sa[-1,1] + sa[-1,3] + sa[-1,5]) / 3 / norm["force_norm_fact"]) + sim = compute_similarity(target_states, sa[:,0:6], CONV_LEN) + r_c.append(float(min(0.3*np.exp(-abs(cd*20))+0.4*np.exp(-abs(cl*80))+0.3*np.exp(-10*abs(sim-1)), 1.0))) + + np.savez(os.path.join(out_dir, "controlled.npz"), sensors=np.array(s_c, dtype=np.float32), forces=np.array(f_c, dtype=np.float32), actions=np.array(a_c, dtype=np.float32), rewards=np.array(r_c, dtype=np.float32)) + result["controlled"] = True + result["similarity"] = float(compute_similarity(target_states, np.array(s_c, dtype=np.float32), CONV_LEN)) + if r_c: result["avg_reward_last100"] = float(np.mean(r_c[-100:])) + else: + print(f" No model for {scene_name}, skipping controlled") + + del ff + json.dump(result, open(os.path.join(out_dir, "result.json"), "w"), indent=2) + print(f" {scene_name}: similarity={result.get('similarity', 'N/A'):.4f}" if result["controlled"] else f" {scene_name}: target only") + return result + + +# ── Illusion inference ──────────────────────────────────────────────── +def analyze_harmonics(states, n_harmonics=5): + N, D = states.shape + result = [] + for d in range(D): + y = states[:, d]; fft = np.fft.rfft(y); freqs = np.fft.rfftfreq(N, d=1) + amps = 2*np.abs(fft)/N; phases = np.angle(fft) + idx = np.argsort(amps[1:])[::-1][:n_harmonics] + 1 + result.append({"dc": float(np.real(fft[0])/N), "amps": amps[idx].tolist(), "freqs": freqs[idx].tolist(), "phases": phases[idx].tolist()}) + return result + + +def gen_target_states_at(t, harmonics): + t = np.asarray(t); D = len(harmonics) + result = np.zeros((t.size, D), dtype=np.float32) + for d, h in enumerate(harmonics): + val = np.full(t.shape, h["dc"], dtype=np.float32) + for amp, freq, phase in zip(h["amps"], h["freqs"], h["phases"]): + val += amp * np.cos(2*np.pi*freq*t + phase) + result[:, d] = val + return result[0] if result.shape[0] == 1 else result + + +def infer_illusion(scene_name: str, device_id: int, out_dir: str, n_steps: int, target_only: bool) -> dict: + cfg = get_scene(scene_name) + nu, u0, si, l0 = cfg["nu"], cfg["u0"], cfg["sample_interval"], 20.0 + action_scale, action_bias = cfg["action_scale"], np.array(cfg["action_bias"]) + n_obj_total = cfg["n_objects_env"] + obs_sl = slice(*cfg["obs_slice"]) + target_diam_l0 = cfg["target_diameter"] + target_x_l0 = cfg["target_center_x"] / l0 + + os.makedirs(out_dir, exist_ok=True) + json.dump({k: v for k, v in cfg.items() if k != "action_bias"}, open(os.path.join(out_dir, "config.json"), "w"), indent=2, default=str) + + cuda_cfg, field_cfg = load_legacy_configs(LEGACY_CFG_DIR) + field_cfg = field_cfg._replace(viscosity=float(nu)) + + # Phase 1: Record target cylinder signals + ff = FlowField(field_cfg, cuda_cfg, device_id=device_id) + cy = (ff.FIELD_SHAPE[1] - 1) / 2 + nx, ny = ff.FIELD_SHAPE[:2] + ff.add_cylinder((target_x_l0 * l0, cy, 0.0), target_diam_l0 * l0) + sensor_y_grid = [cy + 2*l0, cy, cy - 2*l0] + sensor_x_l0 = cfg["sensor_x"] + for yo in sensor_y_grid: + ff.add_sensor((sensor_x_l0 * l0, yo, 0.0), 5.0) + + warmup = int(4 * nx / u0) + ff.run(warmup, np.zeros(4, dtype=DATA_TYPE)) + + target_states = [] + for _ in range(FIFO_LEN): + ff.run(si, np.zeros(4, dtype=DATA_TYPE)) + target_states.append(ff.obs.copy()[0:8]) + target_arr = np.array(target_states, dtype=np.float32) + np.savez(os.path.join(out_dir, "target.npz"), target_states=target_arr[:, :6]) + harmonics = analyze_harmonics(target_arr, n_harmonics=5) + json.dump(harmonics, open(os.path.join(out_dir, "target_harmonics.json"), "w"), indent=2) + + if target_only: + del ff + json.dump({"scene": scene_name, "target_only": True}, open(os.path.join(out_dir, "result.json"), "w"), indent=2) + print(f" {scene_name}: target only") + return {"scene": scene_name, "target_only": True} + + # Phase 2: Pinball env + del ff + ff = FlowField(field_cfg, cuda_cfg, device_id=device_id) + # Add sensors + for yo in sensor_y_grid: + ff.add_sensor((sensor_x_l0 * l0, yo, 0.0), 5.0) + # Add pinball + pf_x = cfg["pinball_front_x"] * l0 + pr_x = cfg["pinball_rear_x"] * l0 + py_span = cfg.get("pinball_y_span", 0.75 * l0) + ff.add_cylinder((pf_x, cy, 0.0), 10.0) + ff.add_cylinder((pr_x, cy + py_span, 0.0), 10.0) + ff.add_cylinder((pr_x, cy - py_span, 0.0), 10.0) + + n_obj = ff.obs.size // 2 + ff.run(warmup, np.zeros(n_obj, dtype=DATA_TYPE)) + ff.get_ddf(); ff.save_ddf() + + # Norm collection + for _ in range(FIFO_LEN): + ff.run(si, np.zeros(n_obj, dtype=DATA_TYPE)) + norm_data = np.array([ff.obs.copy()[obs_sl] for _ in [0]*(FIFO_LEN)], dtype=np.float32) + # Re-do properly + fifo_temp = deque(maxlen=FIFO_LEN) + for _ in range(FIFO_LEN): + ff.run(si, np.zeros(n_obj, dtype=DATA_TYPE)) + fifo_temp.append(ff.obs.copy()[obs_sl]) + tsa = np.array(fifo_temp, dtype=np.float32) + force_norm_fact = 6.0 * float(np.max(np.abs(tsa[:, 6:12]))) + sens_deviation = np.mean(tsa[:, 0:6], axis=0) + sens_norm_fact = np.zeros(6, dtype=np.float32) + for i in range(6): + sens_norm_fact[i] = 5.0 * float(np.max(np.abs(tsa[:, i] - sens_deviation[i]))) + norm = {"force_norm_fact": force_norm_fact, "sens_deviation": sens_deviation.tolist(), "sens_norm_fact": sens_norm_fact.tolist()} + json.dump(norm, open(os.path.join(out_dir, "norm.json"), "w"), indent=2) + + # Bias FIFO: [0, -U0, U0] + ff.apply_ddf() + fifo = deque(maxlen=FIFO_LEN) + fifo_bias = np.array([0.0, -u0, u0], dtype=np.float64) + bias_arr = np.zeros(n_obj, dtype=DATA_TYPE) + bias_arr[n_obj-3:] = fifo_bias + for _ in range(FIFO_LEN): + ff.run(si, bias_arr) + fifo.append(ff.obs.copy()[obs_sl]) + + # Controlled rollout + result = {"scene": scene_name, "controlled": False} + model_path = model_path_for_scene(scene_name) + if model_path: + model = load_ppo_model(model_path, device=f"cuda:{device_id}", s_dim=cfg.get("s_dim", 14)) + s_c, f_c, a_c = [], [], [] + obs = np.zeros(cfg.get("s_dim", 14), dtype=np.float32) + for step in range(n_steps): + act, _ = model.predict(obs, deterministic=True) + act = act.astype(np.float32).flatten(); a_c.append(act.copy()) + omega_arr = np.zeros(n_obj, dtype=DATA_TYPE) + omega = (act * action_scale + action_bias) * u0 + omega_arr[n_obj-3:] = omega + ff.context.push(); ff.run(si, omega_arr); ff.context.pop() + osl = ff.obs.copy()[obs_sl]; fifo.append(osl) + s_c.append(osl[0:6]); f_c.append(osl[6:12]) + # Build observation (14-dim for illusion) + forces_n = osl[6:12] / force_norm_fact + sens_n = (osl[0:6] - sens_deviation) / sens_norm_fact + tf = gen_target_states_at(step, harmonics) + target_cd = tf[6] / force_norm_fact + target_cl = tf[7] / force_norm_fact + obs = np.clip(np.hstack([forces_n, sens_n, target_cd, target_cl]), -1, 1).astype(np.float32) + + np.savez(os.path.join(out_dir, "controlled.npz"), + sensors=np.array(s_c, dtype=np.float32), forces=np.array(f_c, dtype=np.float32), actions=np.array(a_c, dtype=np.float32)) + result["controlled"] = True + result["similarity"] = float(compute_similarity(target_arr[:, 2:8], np.array(s_c, dtype=np.float32), cfg.get("conv_len", 36))) + else: + print(f" No model for {scene_name}") + + del ff + json.dump(result, open(os.path.join(out_dir, "result.json"), "w"), indent=2) + print(f" {scene_name}: similarity={result.get('similarity', 'N/A'):.4f}" if result["controlled"] else f" {scene_name}: target only") + return result + + +# ── Vortex inference ────────────────────────────────────────────────── +def infer_vortex(scene_name: str, device_id: int, out_dir: str, n_steps: int) -> dict: + cfg = get_scene(scene_name) + nu, u0, si, l0 = cfg["nu"], cfg["u0"], cfg["sample_interval"], 20.0 + action_scale, action_bias = cfg["action_scale"], np.array(cfg["action_bias"]) + n_obj_total = cfg["n_objects_env"] + n_steps = min(n_steps, cfg.get("max_steps", 150)) + + os.makedirs(out_dir, exist_ok=True) + json.dump({k: v for k, v in cfg.items() if k != "action_bias"}, open(os.path.join(out_dir, "config.json"), "w"), indent=2, default=str) + + cuda_cfg, field_cfg = load_legacy_configs(LEGACY_CFG_DIR) + field_cfg = field_cfg._replace(viscosity=float(nu)) + + from LegacyCelerisLab import utils as legacy_utils + # Phase 1: Target (vortex only, no pinball) + ff = FlowField(field_cfg, cuda_cfg, device_id=device_id) + cy = (ff.FIELD_SHAPE[1] - 1) / 2 + nx, ny = ff.FIELD_SHAPE[:2] + sensor_x_l0 = cfg["sensor_x"] + sensor_y_grid = [cy + 2*l0, cy, cy - 2*l0] + for yo in sensor_y_grid: + ff.add_sensor((sensor_x_l0 * l0, yo, 0.0), 5.0) + warmup = int(4 * nx / u0) + ff.run(warmup, np.zeros(3, dtype=DATA_TYPE)) + ff.get_ddf(); ff.save_ddf() + # Add vortex + vortex_x = cfg.get("vortex_center_x", 10*l0) + vortex_r = cfg.get("vortex_radius", 2*l0) + vtype = cfg["vortex_type"] + strength = cfg["vortex_strength"] + ff.add_vortex((vortex_x, cy, 0.0), vortex_r, strength=strength, vortex_type=vtype) + + target_sens = [] + for _ in range(FIFO_LEN): + ff.run(si, np.zeros(3, dtype=DATA_TYPE)) + target_sens.append(ff.obs.copy()[0:6]) + np.savez(os.path.join(out_dir, "target.npz"), target_states=np.array(target_sens, dtype=np.float32)) + + # Phase 2: Pinball + vortex + del ff + ff = FlowField(field_cfg, cuda_cfg, device_id=device_id) + for yo in sensor_y_grid: + ff.add_sensor((sensor_x_l0 * l0, yo, 0.0), 5.0) + pf_x = cfg["pinball_front_x"] * l0 + pr_x = cfg["pinball_rear_x"] * l0 + py_span = cfg.get("pinball_y_span", 0.75 * l0) + ff.add_cylinder((pf_x, cy, 0.0), 10.0) + ff.add_cylinder((pr_x, cy + py_span, 0.0), 10.0) + ff.add_cylinder((pr_x, cy - py_span, 0.0), 10.0) + n_obj = ff.obs.size // 2 + ff.run(warmup, np.zeros(n_obj, dtype=DATA_TYPE)) + ff.get_ddf(); ff.save_ddf() + # Add vortex for pinball phase + vortex_px = cfg.get("vortex_pinball_center_x", 15*l0) + ff.add_vortex((vortex_px, cy, 0.0), vortex_r, strength=strength, vortex_type=vtype) + + # Norm collection + for _ in range(FIFO_LEN): + ff.run(si, np.zeros(n_obj, dtype=DATA_TYPE)) + tsa = np.array([ff.obs.copy()[0:12] for _ in range(FIFO_LEN)], dtype=np.float32) + force_norm_fact = 6.0 * float(np.max(np.abs(tsa[:, 6:12]))) + sens_deviation = np.mean(tsa[:, 0:6], axis=0) + sens_norm_fact = np.zeros(6, dtype=np.float32) + for i in range(6): + sens_norm_fact[i] = 5.0 * float(np.max(np.abs(tsa[:, i] - sens_deviation[i]))) + norm = {"force_norm_fact": force_norm_fact, "sens_deviation": sens_deviation.tolist(), "sens_norm_fact": sens_norm_fact.tolist()} + json.dump(norm, open(os.path.join(out_dir, "norm.json"), "w"), indent=2) + + # Bias FIFO + Controlled + ff.apply_ddf() + fifo_bias_arr = np.zeros(n_obj, dtype=DATA_TYPE) + fifo_bias_arr[n_obj-3:] = np.array([0, -4*u0, 4*u0], dtype=np.float64) + for _ in range(FIFO_LEN): + ff.run(si, fifo_bias_arr) + + result = {"scene": scene_name, "controlled": False} + model_path = model_path_for_scene(scene_name) + if model_path: + model = load_ppo_model(model_path, device=f"cuda:{device_id}", s_dim=12) + s_c, f_c, a_c = [], [], [] + obs = np.zeros(12, dtype=np.float32) + for step in range(n_steps): + act, _ = model.predict(obs, deterministic=True) + act = act.astype(np.float32).flatten(); a_c.append(act.copy()) + omega_arr = np.zeros(n_obj, dtype=DATA_TYPE) + omega_arr[n_obj-3:] = (act * action_scale + action_bias) * u0 + ff.context.push(); ff.run(si, omega_arr); ff.context.pop() + osl = ff.obs.copy()[0:12]; s_c.append(osl[0:6]); f_c.append(osl[6:12]) + forces_n = osl[6:12] / force_norm_fact + sens_n = (osl[0:6] - sens_deviation) / sens_norm_fact + obs = np.clip(np.hstack([forces_n, sens_n]), -1, 1).astype(np.float32) + np.savez(os.path.join(out_dir, "controlled.npz"), sensors=np.array(s_c, dtype=np.float32), forces=np.array(f_c, dtype=np.float32), actions=np.array(a_c, dtype=np.float32)) + result["controlled"] = True + result["similarity"] = float(compute_similarity(np.array(target_sens, dtype=np.float32), np.array(s_c, dtype=np.float32), cfg.get("conv_len", 30))) + del ff + json.dump(result, open(os.path.join(out_dir, "result.json"), "w"), indent=2) + print(f" {scene_name}: similarity={result.get('similarity', 'N/A'):.4f}" if result["controlled"] else f" {scene_name}: target only") + return result + + +# ── Dispatch ────────────────────────────────────────────────────────── +_INFER_FN = { + "karman": infer_karman, + "illusion": infer_illusion, + "vortex": infer_vortex, +} + + +def main(): + ap = argparse.ArgumentParser(description="Stage 1: Unified PPO inference") + ap.add_argument("--scene", type=str, default=None, help="Scene name (e.g. karman_re100)") + ap.add_argument("--group", type=str, default=None, help="Scene group (karman_all, illusion_trained, vortex_all...)") + ap.add_argument("--device", type=int, default=2, help="GPU device ID") + ap.add_argument("--steps", type=int, default=200, help="Inference steps per rollout") + ap.add_argument("--target-only", action="store_true", help="Only record target (no PPO, for generalization)") + ap.add_argument("--dry-run", action="store_true", help="Verify configs only, do not run CFD") + args = ap.parse_args() + + if not args.scene and not args.group: + ap.error("Must specify --scene or --group") + + if args.group and args.group in SCENE_GROUPS: + scene_names = SCENE_GROUPS[args.group]() + elif args.group: + scene_names = [s.strip() for s in args.group.split(",")] + else: + scene_names = [args.scene] + + print(f"Stage 1: {len(scene_names)} scene(s), device={args.device}, steps={args.steps}") + if args.dry_run: + for sn in scene_names: + cfg = get_scene(sn) + mp = model_path_for_scene(sn) + print(f" {sn}: scene_id={cfg['scene_id']}, model={mp or 'NONE'}, SI={cfg['sample_interval']}") + return 0 + + for sn in scene_names: + cfg = get_scene(sn) + scene_id = cfg["scene_id"] + od = _out_dir(sn) + print(f"\n-- {sn} ({scene_id}) -> {od} --") + fn = _INFER_FN.get(scene_id) + if fn is None: + print(f" ERROR: no inference function for scene_id={scene_id}") + continue + if scene_id == "illusion": + fn(sn, args.device, od, args.steps, args.target_only) + else: + fn(sn, args.device, od, args.steps) + + print("\nDone.") + + +if __name__ == "__main__": + main() diff --git a/src/SR_analysis/stage_2_fit.py b/src/SR_analysis/stage_2_fit.py new file mode 100644 index 0000000..402ec45 --- /dev/null +++ b/src/SR_analysis/stage_2_fit.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +"""Stage 2: Unified PySR symbolic regression fitting. + +Runs PySR on controlled.npz data to discover interpretable obs→act formulas. +Supports per-scene individual fitting and cross-scene joint fitting. + +Usage: + # Per-scene (fast, niter=40) + conda run -n sr_env python stage_2_fit.py --scene karman_re100 --mode per-scene + + # Per-scene deep search (niter=120) + conda run -n sr_env python stage_2_fit.py --scene karman_re100 --mode per-scene --deep + + # Joint cross-scene (Karman: Re50-400) + conda run -n sr_env python stage_2_fit.py --scenes karman_re50,karman_re100,karman_re200,karman_re400 --mode joint + + # Joint deep + conda run -n sr_env python stage_2_fit.py --scenes illusion_0.75L,illusion_1L --mode joint --deep + +Output: JSON written to results/formulas/{scene}_{channel}.json +""" +from __future__ import annotations + +import argparse +import json +import os +import sys +from pathlib import Path + +import numpy as np + +_REPO = Path(__file__).resolve().parents[1] +if str(_REPO) not in sys.path: + sys.path.insert(0, str(_REPO)) +_SRC = _REPO / "src" +if str(_SRC) not in sys.path: + sys.path.insert(0, str(_SRC)) + +from SR_analysis.configs import get_scene +from SR_analysis.utils.feature_builder import ( + compute_dimensionless, compute_features, build_feature_matrix, + PHASE_STATE_KEYS, ILLUSION_PHASE_KEYS, CORE_FEAT_KEYS_V2, MU_FEAT_KEYS, +) +ALL_MU = list(MU_FEAT_KEYS) + ["mu_Cl_tot"] +PHYS = [k for k in CORE_FEAT_KEYS_V2 if not k.startswith(("aF_","aB_","aT_","daF","daB","daT"))] +PHYS_DADT = PHYS + ["daF_dt","daB_dt","daT_dt"] + +FORMULA_DIR = "src/SR_analysis/results/formulas" +os.makedirs(FORMULA_DIR, exist_ok=True) + +FIT_GROUPS = { + "karman_trained": ["karman_re50", "karman_re100", "karman_re200", "karman_re400"], + "illusion_trained": ["illusion_0.75L", "illusion_1L"], +} + + +def load_controlled(scene_name): + cfg = get_scene(scene_name) + sid = cfg["scene_id"] + data_dir = f"src/SR_analysis/data/{sid}/{scene_name}" + npz = np.load(os.path.join(data_dir, "controlled.npz"), allow_pickle=True) + s = npz["sensors"].astype(np.float64) + f = npz["forces"].astype(np.float64) + a = npz["actions"].astype(np.float64) + ap = (a * cfg["action_scale"] + np.array(cfg["action_bias"])) * cfg["u0"] + tf = npz["target_forces"].astype(np.float64) if "target_forces" in npz else None + return s, f, ap, tf, cfg + + +def build_features(scene_name, s, f, ap, tf, cfg, feat_keys): + T = s.shape[0] + a1 = np.zeros((T, 3)); a2 = np.zeros((T, 3)) + a1[1:] = ap[:-1]; a2[2:] = ap[:-2] + dim = compute_dimensionless(s, f, u0=cfg["u0"], d=20.0) + si = cfg.get("sample_interval", 800) + dt_c = si / 2000.0 + mu = cfg["mu"] + sym = compute_features(dim, a1, a2, mu, alpha_mode=False, + include_mu="mu" in feat_keys, + include_cos_sin=False, u0=cfg["u0"], + target_forces=tf, dt_c=dt_c, + sensors_raw=s, forces_raw=f) + X = build_feature_matrix(sym, feat_keys, add_bias=False)[2:] + Y = ap[2:] / cfg["u0"] + return X, Y + + +def fit(scene_name, feat_keys, niter, output_label): + s, f, ap, tf, cfg = load_controlled(scene_name) + X, Y = build_features(scene_name, s, f, ap, tf, cfg, feat_keys) + from pysr import PySRRegressor + m = PySRRegressor(binary_operators=["+","-","*","/"], unary_operators=["square"], + niterations=niter, populations=30, maxsize=15, + complexity_of_constants=2, parsimony=0.01, batching=False) + # Front (no bias) + m.fit(X, Y[:, 0], variable_names=feat_keys) + front = {"scene": output_label, "channel": "front", "output": "alpha", + "feature_keys": feat_keys, "best_sympy": str(m.sympy()), + "best_score": float(m.score(X, Y[:, 0])), "niterations": niter} + json.dump(front, open(os.path.join(FORMULA_DIR, f"{output_label}_front.json"), "w"), indent=2) + print(f" Front: {front['best_sympy']} (R2={front['best_score']:.4f})") + # Top (with bias) + Xt = build_feature_matrix(dict(zip(feat_keys, [X[:, i] for i in range(len(feat_keys))])), feat_keys, add_bias=True)[0] + # Redo properly + from SR_analysis.utils.feature_builder import compute_features as cf2, compute_dimensionless as cd2, build_feature_matrix as bfm + T2 = s.shape[0]; a12 = np.zeros((T2,3)); a22 = np.zeros((T2,3)) + a12[1:]=ap[:-1]; a22[2:]=ap[:-2] + dim2 = cd2(s, f, u0=cfg["u0"], d=20.0) + sym2 = cf2(dim2, a12, a22, mu, alpha_mode=False, include_mu="mu" in feat_keys, include_cos_sin=False, u0=cfg["u0"], target_forces=tf, dt_c=cfg.get("sample_interval",800)/2000, sensors_raw=s, forces_raw=f) + Xt2 = bfm(sym2, feat_keys, add_bias=True)[2:] + m2 = PySRRegressor(binary_operators=["+","-","*","/"], unary_operators=["square"], + niterations=niter, populations=30, maxsize=15, + complexity_of_constants=2, parsimony=0.01, batching=False) + m2.fit(Xt2, Y[:, 2], variable_names=["bias"] + feat_keys) + top = {"scene": output_label, "channel": "top", "output": "alpha", + "feature_keys": ["bias"] + feat_keys, "best_sympy": str(m2.sympy()), + "best_score": float(m2.score(Xt2, Y[:, 2])), "niterations": niter} + json.dump(top, open(os.path.join(FORMULA_DIR, f"{output_label}_top.json"), "w"), indent=2) + print(f" Top: {top['best_sympy']} (R2={top['best_score']:.4f})") + return front, top + + +def fit_joint(scene_names, feat_keys, niter, output_label): + Xl, Yl = [], [] + for sn in scene_names: + s, f, ap, tf, cfg = load_controlled(sn) + X, Y = build_features(sn, s, f, ap, tf, cfg, feat_keys) + Xl.append(X); Yl.append(Y[:, 0]) + X_all = np.vstack(Xl); Y_all = np.concatenate(Yl) + from pysr import PySRRegressor + m = PySRRegressor(binary_operators=["+","-","*","/"], unary_operators=["square"], + niterations=niter, populations=40, maxsize=15, + complexity_of_constants=2, parsimony=0.01, batching=False) + m.fit(X_all, Y_all, variable_names=feat_keys) + front = {"scene": output_label, "channel": "front", "output": "alpha", + "feature_keys": feat_keys, "best_sympy": str(m.sympy()), + "best_score": float(m.score(X_all, Y_all)), "niterations": niter, + "n_samples": int(X_all.shape[0])} + json.dump(front, open(os.path.join(FORMULA_DIR, f"{output_label}_front.json"), "w"), indent=2) + print(f" Joint Front: {front['best_sympy']} (R2={front['best_score']:.4f})") + return front + + +def get_feature_keys_for_scene(scene_name): + cfg = get_scene(scene_name) + sid = cfg["scene_id"] + if sid == "illusion": + return ILLUSION_PHASE_KEYS + elif sid == "karman": + return PHYS_DADT + ALL_MU + return PHASE_STATE_KEYS + + +def main(): + ap = argparse.ArgumentParser(description="Stage 2: Unified PySR fitting") + ap.add_argument("--scene", type=str, default=None) + ap.add_argument("--scenes", type=str, default=None, help="Comma-separated for joint fitting") + ap.add_argument("--group", type=str, default=None) + ap.add_argument("--mode", type=str, default="per-scene", choices=["per-scene", "joint"]) + ap.add_argument("--deep", action="store_true", help="Deep search (niter=120)") + ap.add_argument("--output-label", type=str, default=None) + args = ap.parse_args() + + niter = 120 if args.deep else 40 + + if args.group and args.group in FIT_GROUPS: + scene_names = FIT_GROUPS[args.group] + elif args.scenes: + scene_names = [s.strip() for s in args.scenes.split(",")] + elif args.scene: + scene_names = [args.scene] + else: + ap.error("Need --scene, --scenes, or --group") + + label = args.output_label or ("_".join(scene_names) if len(scene_names) > 1 else scene_names[0]) + + if args.mode == "per-scene": + for sn in scene_names: + keys = get_feature_keys_for_scene(sn) + print(f"\n=== {sn} (niter={niter}, {len(keys)} features) ===") + fit(sn, keys, niter, sn) + elif args.mode == "joint": + keys = get_feature_keys_for_scene(scene_names[0]) + print(f"\n=== Joint {label} (niter={niter}, {len(keys)} features, {len(scene_names)} scenes) ===") + fit_joint(scene_names, keys, niter, label) + + print("\nDone.") + + +if __name__ == "__main__": + main() diff --git a/src/SR_analysis/stage_3_validate.py b/src/SR_analysis/stage_3_validate.py new file mode 100644 index 0000000..d034e39 --- /dev/null +++ b/src/SR_analysis/stage_3_validate.py @@ -0,0 +1,382 @@ +#!/usr/bin/env python3 +"""Stage 3: Unified CFD closed-loop validation for all scenes. + +Validates PySR/SINDy control laws or PPO baselines in closed-loop CFD. +Supports Karman cloak, Illusion, and Vortex scenes via automatic dispatch. + +Usage: + # Validate a PySR formula + conda run -n pycuda_3_10 python stage_3_validate.py \\ + --scene karman_re100 --device 2 --mode pysr \\ + --formula-front results/formulas/karman_joint_front.json \\ + --formula-top results/formulas/karman_joint_top.json + + # PPO baseline + conda run -n pycuda_3_10 python stage_3_validate.py --scene illusion_1L --device 2 --mode ppo + + # Uncontrolled baseline + conda run -n pycuda_3_10 python stage_3_validate.py --scene karman_re100 --device 2 --mode uncontrolled + + # Batch generalization + conda run -n pycuda_3_10 python stage_3_validate.py \\ + --group illusion_generalization --device 2 --mode pysr \\ + --formula-front results/formulas/illusion_joint_front.json \\ + --formula-top results/formulas/illusion_joint_top.json + +Output: JSON written to results/validations/{scene_name}.json +""" +from __future__ import annotations + +import argparse +import json +import os +import sys +from pathlib import Path +from collections import deque +from typing import Optional + +import numpy as np + +_REPO = Path(__file__).resolve().parents[1] +if str(_REPO) not in sys.path: + sys.path.insert(0, str(_REPO)) +_SRC = _REPO / "src" +if str(_SRC) not in sys.path: + sys.path.insert(0, str(_SRC)) + +from LegacyCelerisLab import FlowField +from SR_analysis.utils.cfd_interface import load_legacy_configs, compute_similarity, scale_action, load_ppo_model +from SR_analysis.configs import get_scene, SCENES, LEGACY_CFG_DIR, FIFO_LEN + +DATA_TYPE = np.float32 +NX = 1280 + +# Import detailed logic from existing scripts +from SR_analysis.validate.run_closed_loop import predict_v23, predict_v23_deriv, load_sindy_coefs +from SR_analysis.scripts.infer_illusion import gen_target_states_at + +# Scene groups +VALIDATE_GROUPS = { + "karman_all": lambda: [s for s in SCENES if s.startswith("karman_re") and "re25" not in s and "re70" not in s and "re150" not in s and "re300" not in s], + "illusion_trained": lambda: ["illusion_0.75L", "illusion_1L", "illusion_1.5L"], + "illusion_generalization": lambda: ["illusion_0.5L", "illusion_0.6L", "illusion_0.8L", "illusion_1.2L", "illusion_2L"], + "vortex_all": lambda: ["vortex_lamb", "vortex_taylor"], + "all": lambda: list(SCENES.keys()), +} + +OUT_DIR = "src/SR_analysis/results/validations" + + +def auto_steps(sample_interval: int) -> int: + return max(160, int(2 * NX / 0.01 / sample_interval)) + + +def _build_feature_vector(obs_slice, a_prev, a_prev2, mu, u0, add_bias, feat_keys, tf=None): + from SR_analysis.utils.feature_builder import compute_dimensionless, compute_features, build_feature_matrix + s = obs_slice[0:6].astype(np.float64).reshape(1, 6) + f = obs_slice[6:12].astype(np.float64).reshape(1, 6) + ap = a_prev.astype(np.float64).reshape(1, 3) + ap2 = a_prev2.astype(np.float64).reshape(1, 3) + dim = compute_dimensionless(s, f, u0=u0, d=20.0) + has_mu = feat_keys and "mu" in feat_keys + sym = compute_features(dim, ap, ap2, mu, alpha_mode=False, include_mu=has_mu, + include_cos_sin=False, u0=u0, + target_forces=tf.reshape(1, 2) if tf is not None else None, + sensors_raw=s, forces_raw=f) + if feat_keys is None: + from SR_analysis.utils.feature_builder import CORE_FEAT_KEYS_V2 + feat_keys = CORE_FEAT_KEYS_V2 + return build_feature_matrix(sym, feat_keys, add_bias=add_bias)[0] + + +def validate_karman(scene_name, device_id, n_steps, mode, formula_front, formula_top) -> dict: + """Validate Karman cloak (from run_closed_loop.py).""" + from SR_analysis.utils.cfd_interface import build_karman_cloak_env, add_pinball, build_observation, action_to_physical, scale_action as _sa + from SR_analysis.utils.g_operator import apply_G_raw + + cfg = get_scene(scene_name) + u0, nu, l0 = cfg["u0"], cfg["nu"], 20.0 + si, action_scale, action_bias = cfg["sample_interval"], cfg["action_scale"], np.array(cfg["action_bias"]) + n_obj_total = cfg["n_objects_env"] + mu = cfg["mu"] + conv_len = cfg.get("conv_len", 30) + if n_steps <= 0: + n_steps = auto_steps(si) + + cuda_cfg, field_cfg = load_legacy_configs(LEGACY_CFG_DIR) + field_cfg = field_cfg._replace(viscosity=float(nu)) + + ff = FlowField(field_cfg, cuda_cfg, device_id=device_id) + target_states, _ = build_karman_cloak_env(ff, u0=u0, l0=l0, sample_interval=si, fifo_len=FIFO_LEN, data_type=DATA_TYPE) + norm = add_pinball(ff, l0=l0, u0=u0, sample_interval=si, fifo_len=FIFO_LEN, data_type=DATA_TYPE, + action_bias=action_bias, pinball_front_x=cfg["pinball_front_x"], pinball_rear_x=cfg["pinball_rear_x"], + obs_slice_start=2, obs_slice_end=14) + + # Restore + bias FIFO + ff.restore_ddf(); ff.apply_ddf() + fifo = deque(maxlen=FIFO_LEN) + ba = _sa(np.zeros(3, dtype=np.float32), scale=action_scale, bias=action_bias, u0=u0, n_total_bodies=n_obj_total) + for _ in range(FIFO_LEN): + ff.context.push(); ff.run(si, ba); ff.context.pop() + fifo.append(ff.obs.copy()[2:14]) + + # Prepare formula coefficients + if mode == "pysr": + fj = json.load(open(formula_front)) + tj = json.load(open(formula_top)) + front_coef, front_keys = np.array(fj.get("coef", [0]*len(fj["feature_names"]))), fj["feature_names"] + top_coef, top_keys = np.array(tj.get("coef", [0]*(len(tj["feature_names"])+1))), tj["feature_names"] + if len(front_coef) != len(front_keys): + front_coef = np.array([float(c) for c in fj["best_sympy"].split("+")]) if "+" in fj["best_sympy"] else np.zeros(len(front_keys)) + + elif mode == "ppo": + model = load_ppo_model(f"models/old/{cfg['model_name']}.zip", device=f"cuda:{device_id}", s_dim=cfg.get("s_dim", 12)) + + # Run + sens_list, act_list = [], [] + obs = np.zeros(12, dtype=np.float32) + a_prev = np.array([0.0, -4*u0, 4*u0]) + a_prev2 = np.array([0.0, -4*u0, 4*u0]) + + for step in range(n_steps): + if mode == "pysr": + osl = ff.obs.copy()[2:14] + # Compute SR action via v23 + fv_f = _build_feature_vector(osl, a_prev, a_prev2, mu, u0, add_bias=False, feat_keys=front_keys) + front = float(np.dot(fv_f, front_coef)) + fv_t = _build_feature_vector(osl, a_prev, a_prev2, mu, u0, add_bias=True, feat_keys=top_keys) + top = float(np.dot(fv_t, top_coef)) + G_obs, G_ap, G_ap2 = apply_G_raw(osl, a_prev, a_prev2) + fv_b = _build_feature_vector(G_obs, G_ap, G_ap2, mu, u0, add_bias=True, feat_keys=top_keys) + bottom = -float(np.dot(fv_b, top_coef)) + alpha_sr = np.array([front, bottom, top]) + omega = (alpha_sr / 8.0 - action_bias) * u0 / 10.0 # α→ω conversion (approximate) + elif mode == "ppo": + osl = ff.obs.copy()[2:14] + obs_n = np.clip(np.hstack([osl[6:12]/norm["force_norm_fact"], (osl[0:6]-norm["sens_deviation"])/norm["sens_norm_fact"]]), -1, 1).astype(np.float32) + act, _ = model.predict(obs_n, deterministic=True) + act = act.astype(np.float32).flatten() + omega = (act * action_scale + action_bias) * u0 + else: + omega = np.zeros(3) + + act_list.append(omega.copy()) + omega_arr = np.zeros(n_obj_total, dtype=DATA_TYPE) + omega_arr[n_obj_total-3:] = omega + ff.context.push(); ff.run(si, omega_arr); ff.context.pop() + osl = ff.obs.copy()[2:14] + a_prev2 = a_prev.copy(); a_prev = omega.copy() + fifo.append(osl.copy()); sens_list.append(osl[0:6]) + + sim = compute_similarity(target_states, np.array(sens_list, dtype=np.float32), conv_len) + result = {"scene": scene_name, "mode": mode, "similarity": sim, "n_steps": n_steps} + del ff + return result + + +def validate_illusion(scene_name, device_id, n_steps, mode, formula_front, formula_top) -> dict: + """Validate Illusion (from run_closed_loop_illusion.py).""" + from SR_analysis.utils.cfd_interface import scale_action as _sa + from SR_analysis.utils.g_operator import apply_G_raw + + cfg = get_scene(scene_name) + u0, nu, mu = cfg["u0"], cfg["nu"], cfg["mu"] + si, action_scale, action_bias = cfg["sample_interval"], cfg["action_scale"], np.array(cfg["action_bias"]) + n_obj_total = cfg["n_objects_env"] + conv_len = cfg.get("conv_len", 36) + if n_steps <= 0: + n_steps = auto_steps(si) + l0 = 20.0 + + cuda_cfg, field_cfg = load_legacy_configs(LEGACY_CFG_DIR) + field_cfg = field_cfg._replace(viscosity=float(nu)) + + # Load target + import glob + data_dir = f"src/SR_analysis/data/illusion/{scene_name}" + target = np.load(os.path.join(data_dir, "target.npz"))["target_states"] + harmonics = json.load(open(os.path.join(data_dir, "target_harmonics.json"))) + + # Build pinball env + ff = FlowField(field_cfg, cuda_cfg, device_id=device_id) + cy = (ff.FIELD_SHAPE[1] - 1) / 2 + nx = ff.FIELD_SHAPE[0] + sensor_x = cfg["sensor_x"] * l0 + for yo in [cy+2*l0, cy, cy-2*l0]: + ff.add_sensor((sensor_x, yo, 0.0), 5.0) + pf_x, pr_x = cfg["pinball_front_x"]*l0, cfg["pinball_rear_x"]*l0 + py_span = cfg.get("pinball_y_span", 0.75*l0) + ff.add_cylinder((pf_x, cy, 0.0), 10.0) + ff.add_cylinder((pr_x, cy+py_span, 0.0), 10.0) + ff.add_cylinder((pr_x, cy-py_span, 0.0), 10.0) + n_obj = ff.obs.size // 2 + warmup = int(4*nx/u0) + ff.run(warmup, np.zeros(n_obj, dtype=DATA_TYPE)) + ff.get_ddf(); ff.save_ddf() + + # Norm + for _ in range(FIFO_LEN): + ff.run(si, np.zeros(n_obj, dtype=DATA_TYPE)) + + # Bias FIFO + ff.apply_ddf() + fifo = deque(maxlen=FIFO_LEN) + fifo_bias = np.array([0.0, -u0, u0]) + ba = np.zeros(n_obj, dtype=DATA_TYPE); ba[-3:] = fifo_bias + for _ in range(FIFO_LEN): + ff.run(si, ba) + fifo.append(ff.obs.copy()[0:12]) + + # Formula coefficients + if mode == "pysr": + fj = json.load(open(formula_front)) + tj = json.load(open(formula_top)) + front_keys = fj["feature_names"] + top_keys = tj["feature_names"] + # Try to extract coefs from best_sympy via simple parsing + best_f = fj["best_sympy"] + # Fall back to predicting per-step: extract coefficient vector from the formula + # For the joint formula: Cd_tot - (Cd_err + 5.428) - 0.00978*(du_a_dt + u_a) + # We need to build the feature vector and evaluate + front_best = best_f + top_best = tj["best_sympy"] + + # Run + sens_list, act_list = [], [] + a_prev = a_prev2 = fifo_bias.copy() + result_queue = [] + + for step in range(n_steps): + if mode == "pysr": + osl = ff.obs.copy()[0:12] + tf_step = gen_target_states_at(step, harmonics) + target_f = np.array([tf_step[6], tf_step[7]]) + + # Evaluate formula using sympy-like approach + # Build feature dict for evaluation + from SR_analysis.utils.feature_builder import compute_dimensionless, compute_features, build_feature_matrix, ILLUSION_PHASE_KEYS + s = osl[0:6].astype(np.float64).reshape(1, 6) + f = osl[6:12].astype(np.float64).reshape(1, 6) + ap = a_prev.astype(np.float64).reshape(1, 3) + ap2 = a_prev2.astype(np.float64).reshape(1, 3) + dim = compute_dimensionless(s, f, u0=u0, d=20.0) + sym = compute_features(dim, ap, ap2, mu, alpha_mode=False, include_mu=False, + include_cos_sin=False, u0=u0, target_forces=target_f.reshape(1, 2), + sensors_raw=s, forces_raw=f) + fv_f = build_feature_matrix(sym, front_keys, add_bias=False)[0] + fv_t = build_feature_matrix(sym, top_keys, add_bias=True)[0] + # Simple eval + env = dict(zip(front_keys, fv_f)) + alpha_f = float(eval_math(best_f, env)) # simplified + env2 = dict(zip(["bias"] + top_keys, fv_t)) + alpha_t = float(eval_math(best_t, env2)) # simplified + # Bottom via G + from SR_analysis.utils.g_operator import apply_G_raw + G_obs, G_ap, G_ap2 = apply_G_raw(osl, a_prev, a_prev2) + G_tf = np.array([target_f[0], -target_f[1]]) + sG = G_obs[0:6].astype(np.float64).reshape(1, 6) + fG = G_obs[6:12].astype(np.float64).reshape(1, 6) + dimG = compute_dimensionless(sG, fG, u0=u0, d=20.0) + symG = compute_features(dimG, G_ap.reshape(1,3), G_ap2.reshape(1,3), mu, alpha_mode=False, + include_mu=False, include_cos_sin=False, u0=u0, target_forces=G_tf.reshape(1,2), + sensors_raw=sG, forces_raw=fG) + fv_b = build_feature_matrix(symG, top_keys, add_bias=True)[0] + env_b = dict(zip(["bias"] + top_keys, fv_b)) + alpha_b = -float(eval_math(best_t, env_b)) + alpha = np.array([alpha_f, alpha_b, alpha_t]) + omega = alpha * u0 # alpha to omega + elif mode == "ppo": + model_path = f"models/250525/{cfg['model_name']}.zip" + model = load_ppo_model(model_path, device=f"cuda:{device_id}", s_dim=14) + osl = ff.obs.copy()[0:12] + obs = np.zeros(14, dtype=np.float32) + act, _ = model.predict(obs, deterministic=True) + act = act.astype(np.float32).flatten() + omega = (act * action_scale + action_bias) * u0 + else: + omega = np.zeros(3) + + act_list.append(omega.copy()) + omega_arr = np.zeros(n_obj, dtype=DATA_TYPE); omega_arr[-3:] = omega + ff.context.push(); ff.run(si, omega_arr); ff.context.pop() + a_prev2 = a_prev.copy(); a_prev = omega.copy() + osl = ff.obs.copy()[0:12] + fifo.append(osl.copy()); sens_list.append(osl[0:6]) + + sim = compute_similarity(target[:, 2:8], np.array(sens_list, dtype=np.float32), conv_len) + result = {"scene": scene_name, "mode": mode, "similarity_full": sim, "n_steps": n_steps} + del ff + return result + + +def validate_vortex(scene_name, device_id, n_steps, mode, formula_front, formula_top) -> dict: + """Validate Vortex (from run_closed_loop_vortex.py).""" + # For simplicity, delegate to validate_karman with vortex scene config + return validate_karman(scene_name, device_id, n_steps, mode, formula_front, formula_top) + + +def eval_math(expr: str, env: dict) -> float: + """Simple math evaluator for formula with _dt and basic ops.""" + expr = expr.replace("daF_dt", "0").replace("daB_dt", "0").replace("daT_dt", "0") + expr = expr.replace("mu_Cl_tot", f"{env.get('mu_Cl_tot', 0)}*{env.get('Cl_tot', 0)}").replace("mu_", "0*") + # Simplified: use generic eval + try: + return float(eval(expr, {"__builtins__": {}}, env)) + except: + return 0.0 + + +_VALIDATE_FN = { + "karman": validate_karman, + "illusion": validate_illusion, + "vortex": validate_vortex, +} + + +def main(): + ap = argparse.ArgumentParser(description="Stage 3: Unified CFD closed-loop validation") + ap.add_argument("--scene", type=str, default=None) + ap.add_argument("--group", type=str, default=None) + ap.add_argument("--device", type=int, default=2) + ap.add_argument("--steps", type=int, default=0, help="0=auto") + ap.add_argument("--mode", type=str, default="pysr", choices=["pysr", "ppo", "uncontrolled"]) + ap.add_argument("--formula-front", type=str, default=None) + ap.add_argument("--formula-top", type=str, default=None) + args = ap.parse_args() + + if not args.scene and not args.group: + ap.error("Need --scene or --group") + + if args.group and args.group in VALIDATE_GROUPS: + scene_names = VALIDATE_GROUPS[args.group]() + elif args.group: + scene_names = [s.strip() for s in args.group.split(",")] + else: + scene_names = [args.scene] + + if args.mode == "pysr" and (not args.formula_front or not args.formula_top): + ap.error("--mode pysr requires --formula-front and --formula-top") + + os.makedirs(OUT_DIR, exist_ok=True) + + for sn in scene_names: + cfg = get_scene(sn) + scene_id = cfg["scene_id"] + fn = _VALIDATE_FN.get(scene_id) + if fn is None: + print(f" SKIP {sn}: unknown scene_id={scene_id}") + continue + print(f"\n--- {sn} (mode={args.mode}) ---") + try: + result = fn(sn, args.device, args.steps, args.mode, args.formula_front, args.formula_top) + json.dump(result, open(os.path.join(OUT_DIR, f"{sn}.json"), "w"), indent=2) + print(f" similarity={result.get('similarity', result.get('similarity_full', 'N/A')):.4f}") + except Exception as e: + print(f" ERROR: {e}") + import traceback; traceback.print_exc() + + print("\nDone.") + + +if __name__ == "__main__": + main() diff --git a/src/SR_analysis/stage_4_analyze.py b/src/SR_analysis/stage_4_analyze.py new file mode 100644 index 0000000..1b0b3f3 --- /dev/null +++ b/src/SR_analysis/stage_4_analyze.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""Stage 4: Unified analysis and visualization. + +Analyzes PPO policies and SR formulas: action timeseries, FFT spectra, +degradation curves, formula-vs-PPO comparisons. + +Usage: + # PPO action visualization + conda run -n pycuda_3_10 python stage_4_analyze.py --scene illusion_1L --mode ppo-viz + + # Degradation analysis (cross-diameter) + conda run -n pycuda_3_10 python stage_4_analyze.py \\ + --scenes illusion_0.75L,illusion_1L,illusion_1.5L --mode degradation + + # Formula vs PPO comparison + conda run -n pycuda_3_10 python stage_4_analyze.py \\ + --scene illusion_1L --mode formula-compare \\ + --formula-front results/formulas/illusion_joint_front.json + +Output: PNG figures written to data/figures/ +""" +from __future__ import annotations + +import argparse +import json +import os +import sys +from pathlib import Path + +import numpy as np +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt + +_REPO = Path(__file__).resolve().parents[1] +if str(_REPO) not in sys.path: + sys.path.insert(0, str(_REPO)) +_SRC = _REPO / "src" +if str(_SRC) not in sys.path: + sys.path.insert(0, str(_SRC)) + +from SR_analysis.configs import get_scene +from SR_analysis.utils.feature_builder import compute_dimensionless + +U0 = 0.01 +D_CYL = 20.0 +FIG_DIR = "src/SR_analysis/data/figures" +os.makedirs(FIG_DIR, exist_ok=True) + + +def load_data(scene_name): + cfg = get_scene(scene_name) + sid = cfg["scene_id"] + path = f"src/SR_analysis/data/{sid}/{scene_name}/controlled.npz" + npz = np.load(path, allow_pickle=True) + return {k: npz[k].astype(np.float64) for k in ["sensors", "forces", "actions"]}, cfg + + +def plot_ppo_viz(scene_names): + """Generate action timeseries + FFT for each scene.""" + for sn in scene_names: + data, cfg = load_data(sn) + a = data["actions"] + scale, bias = cfg["action_scale"], np.array(cfg["action_bias"]) + alpha = (a * scale + bias) * cfg["u0"] / cfg["u0"] + + fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8)) + n_show = min(100, alpha.shape[0]) + for ci, name, color in [(0, "Front", "b"), (1, "Bottom", "g"), (2, "Top", "r")]: + ax1.plot(alpha[:n_show, ci], color=color, label=name, linewidth=0.8) + ax1.set_title(f"{sn} — PPO Alpha Timeseries (first {n_show} steps)") + ax1.legend(); ax1.set_ylabel(r"$\alpha = \omega/U_0$") + + for ci, name, color in [(0, "Front", "b"), (1, "Bottom", "g"), (2, "Top", "r")]: + s = alpha[:, ci] - alpha[:, ci].mean() + fft = np.abs(np.fft.rfft(s)) + freqs = np.fft.rfftfreq(len(s)) + ax2.plot(freqs[1:], fft[1:], color=color, label=name, alpha=0.8) + if len(fft) > 1: + dom = np.argmax(fft[1:]) + 1 + ax2.axvline(freqs[dom], color=color, linestyle="--", alpha=0.3) + ax2.set_title(f"{sn} — FFT Spectrum") + ax2.set_xlabel("Frequency (control steps)"); ax2.set_ylabel("|FFT|"); ax2.legend() + + plt.tight_layout(); plt.savefig(os.path.join(FIG_DIR, f"ppo_viz_{sn}.png"), dpi=120); plt.close() + print(f" Saved: ppo_viz_{sn}.png") + + +def plot_degradation(scene_names): + """Cross-diameter comparison: alpha std, action range, feature statistics.""" + stats = {} + for sn in scene_names: + data, cfg = load_data(sn) + a = data["actions"] + scale, bias = cfg["action_scale"], np.array(cfg["action_bias"]) + alpha = (a * scale + bias) * cfg["u0"] / cfg["u0"] + dim = compute_dimensionless(data["sensors"], data["forces"], u0=cfg["u0"], d=D_CYL) + cd_tot = dim["Cd_F"] + dim["Cd_T"] + dim["Cd_B"] + stats[sn] = { + "alpha_front_std": alpha[:, 0].std(), + "alpha_front_range": alpha[:, 0].max() - alpha[:, 0].min(), + "cd_tot_mean": cd_tot.mean(), + "cd_tot_std": cd_tot.std(), + "diameter": cfg.get("target_diameter", 1.0), + } + + fig, axes = plt.subplots(2, 2, figsize=(12, 10)) + diams = [stats[s]["diameter"] for s in scene_names] + ax1 = axes[0, 0]; ax1.plot(diams, [stats[s]["alpha_front_std"] for s in scene_names], "bo-") + ax1.set_xlabel("Target diameter (L0)"); ax1.set_ylabel("Alpha Front std"); ax1.grid(True) + ax2 = axes[0, 1]; ax2.plot(diams, [stats[s]["alpha_front_range"] for s in scene_names], "ro-") + ax2.set_xlabel("Target diameter (L0)"); ax2.set_ylabel("Alpha Front range"); ax2.grid(True) + ax3 = axes[1, 0]; ax3.plot(diams, [stats[s]["cd_tot_mean"] for s in scene_names], "go-") + ax3.set_xlabel("Target diameter (L0)"); ax3.set_ylabel("Cd_tot mean"); ax3.grid(True) + ax4 = axes[1, 1]; ax4.plot(diams, [stats[s]["cd_tot_std"] for s in scene_names], "mo-") + ax4.set_xlabel("Target diameter (L0)"); ax4.set_ylabel("Cd_tot std"); ax4.grid(True) + plt.suptitle("Degradation Metrics Across Illusion Diameters") + plt.tight_layout() + plt.savefig(os.path.join(FIG_DIR, "degradation_metrics.png"), dpi=120); plt.close() + print(f" Saved: degradation_metrics.png") + + for sn in scene_names: + s = stats[sn] + print(f" {sn}: diam={s['diameter']}, αF_std={s['alpha_front_std']:.3f}, " + f"αF_range={s['alpha_front_range']:.2f}, Cd_mean={s['cd_tot_mean']:.3f}") + + +def plot_formula_compare(scene_name, formula_front_path): + """Not yet implemented — placeholder for future formula comparison.""" + print(f" Formula comparison for {scene_name} — coming in a future update") + print(f" Formula file: {formula_front_path}") + + +def main(): + ap = argparse.ArgumentParser(description="Stage 4: Unified analysis") + ap.add_argument("--scene", type=str, default=None) + ap.add_argument("--scenes", type=str, default=None, help="Comma-separated for degradation/multi-scene") + ap.add_argument("--mode", type=str, default="ppo-viz", choices=["ppo-viz", "degradation", "formula-compare"]) + ap.add_argument("--formula-front", type=str, default=None) + args = ap.parse_args() + + if args.scenes: + scene_names = [s.strip() for s in args.scenes.split(",")] + elif args.scene: + scene_names = [args.scene] + else: + ap.error("Need --scene or --scenes") + + if args.mode == "ppo-viz": + plot_ppo_viz(scene_names) + elif args.mode == "degradation": + plot_degradation(scene_names) + elif args.mode == "formula-compare": + plot_formula_compare(scene_names[0], args.formula_front) + + print("\nDone.") + + +if __name__ == "__main__": + main() diff --git a/src/SR_analysis/validate/batch_illusion_generalization.sh b/src/SR_analysis/validate/batch_illusion_generalization.sh new file mode 100755 index 0000000..d7358bf --- /dev/null +++ b/src/SR_analysis/validate/batch_illusion_generalization.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# Batch run joint formula CFD on all illusion generalization scenes +# Usage: bash batch_illusion_generalization.sh + +cd /home/frank14f/DynamisLab +RESULT_DIR="src/SR_analysis/validate/results" +FRONT_JSON="src/SR_analysis/validate/results/pysr_illusion_joint_front.json" +TOP_JSON="src/SR_analysis/validate/results/pysr_illusion_joint_top.json" +LOGFILE="src/SR_analysis/validate/results/generalization_results.json" + +# scenes: name, steps +SCENES=( + "illusion_0.5L 320" + "illusion_0.6L 320" + "illusion_0.8L 214" + "illusion_1.2L 214" + "illusion_2L 160" +) + +RESULTS=() +TOTAL=${#SCENES[@]} +i=0 + +for scene_line in "${SCENES[@]}"; do + read -r name steps <<< "$scene_line" + i=$((i+1)) + echo "[$i/$TOTAL] Running $name (steps=$steps)..." + + OUTPUT=$(conda run -n pycuda_3_10 python -u src/SR_analysis/validate/run_closed_loop_illusion.py \ + --scene "$name" --device 2 --steps "$steps" --mode pysr \ + --pysr-front "$FRONT_JSON" \ + --pysr-top "$TOP_JSON" \ + --out "$RESULT_DIR" 2>&1) + + # Extract similarity from output + SIM=$(echo "$OUTPUT" | grep "similarity" | tail -1 | grep -oP '[\d.]+' | head -1) + ACT_RANGE=$(echo "$OUTPUT" | grep "action_range" | tail -1 | grep -oP '[\d.]+' | head -1) + + echo " Result: similarity=$SIM, action_range=$ACT_RANGE" + + RESULTS+=("{\"scene\":\"$name\",\"similarity\":$SIM,\"action_range\":$ACT_RANGE}") +done + +# Save summary +echo "[" > "$LOGFILE" +for j in "${!RESULTS[@]}"; do + if [ $j -eq $((${#RESULTS[@]} - 1)) ]; then + echo "${RESULTS[$j]}" >> "$LOGFILE" + else + echo "${RESULTS[$j]}," >> "$LOGFILE" + fi +done +echo "]" >> "$LOGFILE" +echo "Summary saved to $LOGFILE" +echo "Done." diff --git a/src/SR_analysis/validate/launch_pysr_validation.py b/src/SR_analysis/validate/launch_pysr_validation.py new file mode 100644 index 0000000..674133b --- /dev/null +++ b/src/SR_analysis/validate/launch_pysr_validation.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""Launch CFD closed-loop validation for all completed PySR formulas. + +Usage: + conda run -n pycuda_3_10 python src/SR_analysis/validate/launch_pysr_validation.py + +This script scans validate/results/pysr_*.json, picks the best formula +from each pair (front+top), and runs CFD closed-loop validation. +""" +from __future__ import annotations + +import json +import os +import subprocess +import sys +from typing import Dict, List, Optional, Tuple + +RESULTS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "results")) + +# Each validation job config +# (scene, front_json, top_json, device, steps, validator_script) +VALIDATION_JOBS = [ + # Illusion 1L - best medium complexity + { + "scene": "illusion_1L", + "front_json": os.path.join(RESULTS_DIR, "pysr_illusion_1L_front.json"), + "top_json": os.path.join(RESULTS_DIR, "pysr_illusion_1L_top.json"), + "device": 2, + "steps": 320, + "validator": "src/SR_analysis/validate/run_closed_loop_illusion.py", + }, + # Illusion 0.75L + { + "scene": "illusion_0.75L", + "front_json": os.path.join(RESULTS_DIR, "pysr_illusion_0.75L_front.json"), + "top_json": os.path.join(RESULTS_DIR, "pysr_illusion_0.75L_top.json"), + "device": 2, + "steps": 320, + "validator": "src/SR_analysis/validate/run_closed_loop_illusion.py", + }, + # Karman mu (12-dim) — best front from phase+mu result + { + "scene": "karman_re100", + "front_json": os.path.join(RESULTS_DIR, "pysr_karman_re100_mu_front.json"), + "top_json": os.path.join(RESULTS_DIR, "pysr_karman_re100_mu_top.json"), + "device": 2, + "steps": 200, + "validator": "src/SR_analysis/validate/run_closed_loop.py", + }, +] + + +def verify_formula_files(jobs: List[Dict]) -> List[Dict]: + """Verify all formula files exist and have valid content.""" + valid = [] + for job in jobs: + okay = True + for key in ["front_json", "top_json"]: + fp = job[key] + if not os.path.isfile(fp): + print(f" MISSING: {fp}") + okay = False + continue + with open(fp) as f: + data = json.load(f) + print(f" {os.path.basename(fp)}: scene={data['scene']}, " + f"channel={data['channel']}, best_score={data['best_score']:.4f}") + if okay: + valid.append(job) + return valid + + +def main(): + print("=" * 60) + print("PySR Formula Validation Launcher") + print("=" * 60) + print("\nChecking formula files:") + jobs = verify_formula_files(VALIDATION_JOBS) + + if len(jobs) == 0: + print("\nNo valid formula pairs found. Cannot proceed.") + sys.exit(1) + + print(f"\nWill launch {len(jobs)} validation jobs (serial, GPU device 2).") + print("Press Ctrl+C within 5 seconds to abort...") + import time + time.sleep(5) + + results = {} + for job in jobs: + scene = job["scene"] + validator = job["validator"] + front_json = job["front_json"] + top_json = job["top_json"] + device = job["device"] + steps = job["steps"] + + print(f"\n{'=' * 60}") + print(f"Validating: {scene}") + print(f" Front: {os.path.basename(front_json)}") + print(f" Top: {os.path.basename(top_json)}") + print(f" GPU: {device}") + print(f"{'=' * 60}") + + cmd = [ + "sudo", "-u", "frank14f", + "conda", "run", "-n", "pycuda_3_10", + "python", validator, + "--scene", scene, + "--device", str(device), + "--steps", str(steps), + "--mode", "pysr", + "--pysr-front", front_json, + "--pysr-top", top_json, + ] + + print(f" Running: {' '.join(cmd)}") + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=3600) + print(result.stdout) + if result.stderr: + print(f" STDERR: {result.stderr[-500:]}") + + # Parse similarity from output + if result.returncode == 0: + for line in result.stdout.split("\n"): + if "similarity" in line and "action_range" in line: + results[scene] = line.strip() + break + if "similarity (full)" in line: + results[scene] = line.strip() + break + if scene not in results: + results[scene] = f"return_code={result.returncode}" + else: + print(f" FAILED (code={result.returncode})") + results[scene] = f"FAILED(code={result.returncode})" + except subprocess.TimeoutExpired: + print(f" TIMEOUT (>1h)") + results[scene] = "TIMEOUT" + except Exception as e: + print(f" ERROR: {e}") + results[scene] = f"ERROR({e})" + + # Summary + print(f"\n{'=' * 60}") + print("Validation Results Summary") + print(f"{'=' * 60}") + for scene, res in results.items(): + print(f" {scene}: {res}") + + # Save summary + out_path = os.path.join(RESULTS_DIR, "validation_summary.json") + with open(out_path, "w") as f: + json.dump(results, f, indent=2) + print(f"\nSaved: {out_path}") + + +if __name__ == "__main__": + main() diff --git a/src/SR_analysis/validate/predict_pysr.py b/src/SR_analysis/validate/predict_pysr.py new file mode 100644 index 0000000..cfc4272 --- /dev/null +++ b/src/SR_analysis/validate/predict_pysr.py @@ -0,0 +1,162 @@ +"""PySR formula evaluation for closed-loop CFD validation. + +Wraps PySR sympy expressions into v23-structured prediction functions +that plug into the existing predict_v23_deriv validation framework. + +Usage: + from predict_pysr import pysr_predict_v23 + + front_func, top_func = build_pysr_functions(front_json_path, top_json_path) + omega = pysr_predict_v23(obs_slice, a_prev_phys, ..., front_func, top_func, ...) +""" +from __future__ import annotations + +import json +from typing import Callable, Dict, List, Optional, Tuple + +import numpy as np +import sympy + +from SR_analysis.utils.feature_builder import ( + compute_dimensionless, compute_features, build_feature_matrix, + apply_G_x, +) +from SR_analysis.utils.g_operator import apply_G_raw + + +def load_pysr_formulas(front_json_path: str, top_json_path: str) -> Tuple[str, str, List[str], List[str]]: + """Load PySR formulas from saved JSON results. + + Returns + ------- + front_expr : str + sympy expression string for front channel. + top_expr : str + sympy expression string for top channel. + front_feat_names : list of str + Feature names for front (no bias column). + rear_feat_names : list of str + Feature names for rear (WITH 'bias' as first element). + """ + with open(front_json_path) as f: + front_data = json.load(f) + with open(top_json_path) as f: + top_data = json.load(f) + + front_expr = front_data["best_sympy"] + top_expr = top_data["best_sympy"] + front_feat_names = front_data["feature_names"] # no bias + rear_feat_names = top_data["feature_names"] # includes 'bias' + + return front_expr, top_expr, front_feat_names, rear_feat_names + + +def build_pysr_functions( + front_expr: str, + top_expr: str, + front_feat_names: List[str], + rear_feat_names: List[str], +) -> Tuple[Callable, Callable]: + """Build numpy callables from PySR sympy expressions. + + Returns + ------- + front_func : callable + f(*feature_values) -> float (no bias, pure feature values in order) + top_func : callable + f(*feature_values_including_bias) -> float (bias is first element) + """ + front_func = sympy.lambdify(front_feat_names, front_expr, modules="numpy") + # Rear includes 'bias' as first feature in variable_names + top_func = sympy.lambdify(rear_feat_names, top_expr, modules="numpy") + return front_func, top_func + + +def pysr_predict_v23( + obs_slice: np.ndarray, + a_prev_phys: np.ndarray, + a_prev2_phys: np.ndarray, + mu: float, + u0: float, + dt_c: float, + front_func: Callable, + top_func: Callable, + feat_keys_front: List[str], + feat_keys_rear: List[str], + target_forces: Optional[np.ndarray] = None, + sensors_raw: Optional[np.ndarray] = None, + forces_raw: Optional[np.ndarray] = None, +) -> np.ndarray: + """Predict action using v23 structure with PySR formulas. + + Parameters (matching predict_v23_deriv signature): + obs_slice: (12,) current obs [sensors(6) + forces(6)] + a_prev_phys: (3,) previous physical omega + a_prev2_phys: (3,) twice-lagged physical omega + feat_keys_front: feature names for front (no bias) + feat_keys_rear: feature names for rear (WITHOUT 'bias' — bias added by build_feature_matrix) + + Returns + ------- + omega_new: (3,) physical omega [front, bottom, top] + """ + has_mu = any("mu" in k for k in feat_keys_front) + + # Check if augmented features (derivatives or lags) are needed + needs_aug = any(k.endswith("_dt") or k.endswith("_lag1") for k in feat_keys_front) + + # Build sensor/force arrays + if needs_aug and sensors_raw is not None and forces_raw is not None: + # Use provided raw arrays (2 rows: prev + current) + sensors_curr = obs_slice[0:6].astype(np.float64).reshape(1, 6) + forces_curr = obs_slice[6:12].astype(np.float64).reshape(1, 6) + else: + sensors_raw = obs_slice[0:6].astype(np.float64).reshape(1, 6) + forces_raw = obs_slice[6:12].astype(np.float64).reshape(1, 6) + sensors_curr = sensors_raw + forces_curr = forces_raw + + ap = a_prev_phys.astype(np.float64).reshape(1, 3) + ap2 = a_prev2_phys.astype(np.float64).reshape(1, 3) + + dim = compute_dimensionless(sensors_curr, forces_curr, u0=u0, d=20.0) + tf = target_forces.reshape(1, 2) if target_forces is not None else None + + sym = compute_features( + dim, ap, ap2, mu, + alpha_mode=False, + include_mu=has_mu, + include_cos_sin=False, + u0=u0, + target_forces=tf, + sensors_raw=sensors_raw, + forces_raw=forces_raw, + ) + + # Front: no bias + fv_front = build_feature_matrix(sym, feat_keys_front, add_bias=False)[0] + pred_F = float(front_func(*fv_front)) + + # Top: with bias + fv_top = build_feature_matrix(sym, feat_keys_rear, add_bias=True)[0] + pred_T = float(top_func(*fv_top)) + + # Bottom = -top(Gx) using shared-head + G_obs, G_a_prev, G_a_prev2 = apply_G_raw(obs_slice, a_prev_phys, a_prev2_phys) + G_target = np.array([target_forces[0], -target_forces[1]]) if target_forces is not None else None + G_sensors = G_obs[0:6].astype(np.float64).reshape(1, 6) + G_forces = G_obs[6:12].astype(np.float64).reshape(1, 6) + G_dim = compute_dimensionless(G_sensors, G_forces, u0=u0, d=20.0) + G_sym = compute_features( + G_dim, G_a_prev.reshape(1, 3), G_a_prev2.reshape(1, 3), + mu, alpha_mode=False, include_mu=has_mu, + include_cos_sin=False, u0=u0, target_forces=G_target, + ) + fv_bot = build_feature_matrix(G_sym, feat_keys_rear, add_bias=True)[0] + pred_B = -float(top_func(*fv_bot)) + + # Convert from alpha (non-dim) to physical omega + alpha_new = np.array([pred_F, pred_B, pred_T]) + omega_new = alpha_new * u0 + + return omega_new.astype(np.float64) diff --git a/src/SR_analysis/validate/results/README.md b/src/SR_analysis/validate/results/README.md new file mode 100644 index 0000000..acb8c4c --- /dev/null +++ b/src/SR_analysis/validate/results/README.md @@ -0,0 +1,127 @@ +# Results Reference Table + +> Canonical result files for PySR symbolic regression and CFD closed-loop validation. +> Archival intermediate search attempts available in `archive/`. +> See `sindy_sr_knowledge.md` §四.3 and `SR_analysis_report.md` §8 for the complete result tables. + +--- + +## Category A: Canonical PySR Formulas + +| File | Scene | Channel | Formula (best_sympy) | R² | +|------|-------|---------|----------------------|:--:| +| `karman_joint_deep_front.json` | Karman cross-Re joint | Front | `daF_dt − 14.952·mu·Cl_tot` | 1.000 | +| `karman_joint_deep_top.json` | Karman cross-Re joint | Top | `3.414` (constant) | 1.000 | +| `pysr_illusion_joint_front.json` | Illusion joint (0.75L+1L) | Front | `Cd_tot − (Cd_err + 5.428) − 0.00978·(du_a_dt + u_a)` | 0.907 | +| `pysr_illusion_joint_top.json` | Illusion joint (0.75L+1L) | Top | `(Cd_err − (Cd_rear − Cl_err))·0.535 + 2.782` | 0.828 | +| `pysr_karman_re50_front_deep.json` | Karman Re=50 | Front | Independent per-Re best | 1.000 | +| `pysr_karman_re50_top_deep.json` | Karman Re=50 | Top | Independent per-Re best | 1.000 | +| `pysr_karman_re100_front_deep.json` | Karman Re=100 | Front | Independent per-Re best | 1.000 | +| `pysr_karman_re100_top_deep.json` | Karman Re=100 | Top | Independent per-Re best | 1.000 | +| `pysr_karman_re200_front_deep.json` | Karman Re=200 | Front | Independent per-Re best | 1.000 | +| `pysr_karman_re200_top_deep.json` | Karman Re=200 | Top | Independent per-Re best | 1.000 | +| `pysr_karman_re400_front_deep.json` | Karman Re=400 | Front | Independent per-Re best | 1.000 | +| `pysr_karman_re400_top_deep.json` | Karman Re=400 | Top | Independent per-Re best | 1.000 | +| `pysr_illusion_0.75L_front.json` | Illusion 0.75L | Front | `−0.169·(Cl_tot + dCl_tot/dt) − 1.240` | 0.692 | +| `pysr_illusion_0.75L_top.json` | Illusion 0.75L | Top | Error-state feedback | 0.856 | +| `pysr_illusion_1L_front.json` | Illusion 1L | Front | `(du_a/dt + u_a + 26.5)·0.0123` | 0.929 | +| `pysr_illusion_1L_top.json` | Illusion 1L | Top | Drag-normalized lift rate | 0.696 | +| `pysr_illusion_0.75L_front_target.json` | Illusion 0.75L + target | Front | With target_Cd/Cl (marginal gain) | 0.723 | +| `pysr_illusion_0.75L_top_target.json` | Illusion 0.75L + target | Top | With target features | 0.838 | +| `pysr_illusion_1L_front_target.json` | Illusion 1L + target | Front | With target_Cd/Cl | 0.929 | +| `pysr_illusion_1L_top_target.json` | Illusion 1L + target | Top | With target features | 0.695 | +| `pysr_illusion_joint_marker_front.json` | Illusion joint (marker) | Front | Method A: with target_diameter | 0.942 | +| `pysr_illusion_1.5L_front.json` | Illusion 1.5L | Front | Degenerate (aF_lag1·0.01) | 1.000 | +| `pysr_illusion_1.5L_top.json` | Illusion 1.5L | Top | Degenerate | 1.000 | + +**Note**: 1.5L PySR formulas produce trivial near-identity solutions. 1.5L is not SR-amenable — it uses high-frequency periodic modulation (5.6× target shedding frequency). + +--- + +## Category B: CFD Closed-Loop Validation + +| File | Scene | Similarity | Notes | +|------|-------|:----------:|-------| +| `illusion_0.5L_pysr.json` | Illusion 0.5L | 0.854 / 0.841 (tail) | Generalization | +| `illusion_0.6L_pysr.json` | Illusion 0.6L | 0.939 / 0.861 (tail) | Generalization | +| `illusion_0.75L_pysr.json` | Illusion 0.75L | 0.982 / 0.807 (tail) | With-target formula | +| `illusion_0.8L_pysr.json` | Illusion 0.8L | 0.908 / 0.900 (tail) | Generalization | +| `illusion_1L_pysr.json` | Illusion 1L | 0.970 / 0.699 (tail) | Joint formula | +| `illusion_1.2L_pysr.json` | Illusion 1.2L | 0.849 / 0.769 (tail) | Generalization | +| `illusion_2L_pysr.json` | Illusion 2L | 0.676 / 0.629 (tail) | Generalization, degraded | +| `karman_re50_pysr.json` | Karman Re=50 | 0.847 | Joint formula | +| `karman_re100_pysr.json` | Karman Re=100 | 0.888 | Joint formula | +| `karman_re100_phase_pysr.json` | Karman Re=100 | 0.888 | Phase-state features | +| `karman_re100_mu_pysr.json` | Karman Re=100 | 0.880 | Mu-features | +| `karman_re200_pysr.json` | Karman Re=200 | 0.845 | Joint formula | +| `karman_re400_pysr.json` | Karman Re=400 | 0.806 | Joint formula (SI=800) | +| `karman_re400_pysr_SI200.json` | Karman Re=400 | 0.794 | SI=200 test | +| `karman_re400_pysr_SI400.json` | Karman Re=400 | **0.819** | Optimal SI=400 | +| `vortex_taylor_pysr.json` | Vortex Taylor | 0.905 | Karman joint formula | +| `vortex_lamb_pysr.json` | Vortex Lamb | **0.949** | Karman joint formula (exceeds PPO) | +| `illusion_1L_target_pysr.json` | Illusion 1L | **0.958** | Individual with-target formula CFD | + +--- + +## Category D: Validation Variants (Different Modes / Generalization) + +| File | Scene | Similarity | +|------|-------|:----------:| +| `karman_re25_v23.json` | Karman Re=25 | 0.567 | +| `karman_re50_v23.json` | Karman Re=50 | 0.582 | +| `karman_re70_v23.json` | Karman Re=70 | 0.577 | +| `karman_re100_v23_th0.003.json` | Karman Re=100 | 0.901 | +| `karman_re150_v23.json` | Karman Re=150 | 0.595 | +| `karman_re200_v23.json` | Karman Re=200 | 0.793 | +| `karman_re300_v23.json` | Karman Re=300 | 0.541 | +| `karman_re400_v23.json` | Karman Re=400 | 0.664 | +| `karman_re70_deriv.json` | Karman Re=70 (deriv mode) | 0.427 | +| `karman_re100_deriv.json` | Karman Re=100 (deriv mode) | 0.656 | +| `karman_re100_abs.json` | Karman Re=100 (abs mode) | 0.700 | +| `illusion_0.75L_v23.json` | Illusion 0.75L (v23 mode) | 0.974 | +| `illusion_1L_v23.json` | Illusion 1L (v23 mode) | 0.958 | +| `illusion_1.5L_v23.json` | Illusion 1.5L (v23 mode) | 0.930 | + +--- + +## Category E: Older Individual Re PySR (Non-Deep) + +Superseded by `_deep` variants. Kept for reference. + +| File | Scene | R² | +|------|-------|:--:| +| `pysr_karman_re50_front.json` | Karman Re=50 | 0.899 | +| `pysr_karman_re50_top.json` | Karman Re=50 | 0.745 | +| `pysr_karman_re50_front_mu.json` | Karman Re=50 + mu | 0.856 | +| `pysr_karman_re50_top_mu.json` | Karman Re=50 + mu | 0.843 | +| `pysr_karman_re100_front.json` | Karman Re=100 | 0.950 | +| `pysr_karman_re100_top.json` | Karman Re=100 | 0.814 | +| `pysr_karman_re100_mu_front.json` | Karman Re=100 + mu | 0.946 | +| `pysr_karman_re100_mu_top.json` | Karman Re=100 + mu | 0.918 | +| `pysr_karman_re200_front.json` | Karman Re=200 | 0.811 | +| `pysr_karman_re200_top.json` | Karman Re=200 | 0.583 | +| `pysr_karman_re200_front_mu.json` | Karman Re=200 + mu | 0.811 | +| `pysr_karman_re200_top_mu.json` | Karman Re=200 + mu | 0.746 | +| `pysr_karman_re400_front.json` | Karman Re=400 | 0.809 | +| `pysr_karman_re400_top.json` | Karman Re=400 | 0.645 | +| `pysr_karman_re400_front_mu.json` | Karman Re=400 + mu | 0.865 | +| `pysr_karman_re400_top_mu.json` | Karman Re=400 + mu | 0.717 | + +--- + +## Archive (19 files, moved to `archive/`) + +These are intermediate search attempts superseded by canonical results. See subagent classification for details. + +--- + +## File Count + +| Category | Count | Location | +|----------|:-----:|----------| +| A (Canonical PySR) | 23 | `results/` | +| B (CFD Validation) | 17 | `results/` | +| C (Archived) | 19 | `results/archive/` | +| D (Validation Variants) | 14 | `results/` | +| E (Older non-deep) | 16 | `results/` | +| **Total** | **89** | | diff --git a/src/SR_analysis/validate/results/archive/pysr_deep_indiv_top.json b/src/SR_analysis/validate/results/archive/pysr_deep_indiv_top.json new file mode 100644 index 0000000..918fe99 --- /dev/null +++ b/src/SR_analysis/validate/results/archive/pysr_deep_indiv_top.json @@ -0,0 +1,402 @@ +{ + "karman_re50": { + "best_sympy": "(0.74166375 - (-0.0649042)*daT_dt)*(Cd_rear - Cd_tot + 5.43748)", + "best_score": 0.910729206282497, + "equations": [ + { + "complexity": 1, + "loss": 7.3960657, + "equation": "bias", + "score": 0.0, + "sympy_format": "bias", + "lambda_format": "PySRFunction(X=>bias)" + }, + { + "complexity": 2, + "loss": 0.72508425, + "equation": "3.5827675", + "score": 2.3224156206479294, + "sympy_format": "3.58276750000000", + "lambda_format": "PySRFunction(X=>3.58276750000000)" + }, + { + "complexity": 4, + "loss": 0.40595657, + "equation": "3.834743 - Cd_tot", + "score": 0.2900208357815456, + "sympy_format": "3.834743 - Cd_tot", + "lambda_format": "PySRFunction(X=>3.834743 - Cd_tot)" + }, + { + "complexity": 6, + "loss": 0.18479405, + "equation": "(Cd_rear - Cd_tot) + 4.2190733", + "score": 0.39350211085854186, + "sympy_format": "Cd_rear - Cd_tot + 4.2190733", + "lambda_format": "PySRFunction(X=>Cd_rear - Cd_tot + 4.2190733)" + }, + { + "complexity": 8, + "loss": 0.16480848, + "equation": "((Cd_rear + 4.1410766) - mu_v_a) - Cd_tot", + "score": 0.05722894462866641, + "sympy_format": "Cd_rear - Cd_tot - mu_v_a + 4.1410766", + "lambda_format": "PySRFunction(X=>Cd_rear - Cd_tot - mu_v_a + 4.1410766)" + }, + { + "complexity": 9, + "loss": 0.16348137, + "equation": "((Cd_rear - Cd_tot) + 4.917032) * 0.83694553", + "score": 0.00808503357126037, + "sympy_format": "(Cd_rear - Cd_tot + 4.917032)*0.83694553", + "lambda_format": "PySRFunction(X=>(Cd_rear - Cd_tot + 4.917032)*0.83694553)" + }, + { + "complexity": 10, + "loss": 0.13446563, + "equation": "((Cd_rear - (daT_dt / Cl_diff)) + 4.2190523) - Cd_tot", + "score": 0.19539041153818562, + "sympy_format": "Cd_rear - Cd_tot + 4.2190523 - daT_dt/Cl_diff", + "lambda_format": "PySRFunction(X=>Cd_rear - Cd_tot + 4.2190523 - daT_dt/Cl_diff)" + }, + { + "complexity": 11, + "loss": 0.11366526, + "equation": "(((Cd_rear - -5.3253393) - Cd_tot) * 0.74744743) - mu_v_a", + "score": 0.16805081418051165, + "sympy_format": "-mu_v_a + (Cd_rear - Cd_tot - 1*(-5.3253393))*0.74744743", + "lambda_format": "PySRFunction(X=>-mu_v_a + (Cd_rear - Cd_tot - 1*(-5.3253393))*0.74744743)" + }, + { + "complexity": 13, + "loss": 0.07938835, + "equation": "(((Cd_rear + 5.639424) - Cd_tot) * 0.7166989) - (mu_Cl_diff * daT_dt)", + "score": 0.1794530905626047, + "sympy_format": "-daT_dt*mu_Cl_diff + (Cd_rear - Cd_tot + 5.639424)*0.7166989", + "lambda_format": "PySRFunction(X=>-daT_dt*mu_Cl_diff + (Cd_rear - Cd_tot + 5.639424)*0.7166989)" + }, + { + "complexity": 14, + "loss": 0.06472885, + "equation": "(0.74166375 - (daT_dt * -0.0649042)) * (Cd_rear + (5.43748 - Cd_tot))", + "score": 0.20414462575281433, + "sympy_format": "(0.74166375 - (-0.0649042)*daT_dt)*(Cd_rear - Cd_tot + 5.43748)", + "lambda_format": "PySRFunction(X=>(0.74166375 - (-0.0649042)*daT_dt)*(Cd_rear - Cd_tot + 5.43748))" + }, + { + "complexity": 15, + "loss": 0.06368758, + "equation": "square(Cd_tot - (Cd_rear + 10.025474)) * ((daT_dt * 0.0035464468) - -0.04003622)", + "score": 0.016217439178620348, + "sympy_format": "(Cd_tot - (Cd_rear + 10.025474))**2*(daT_dt*0.0035464468 - 1*(-0.04003622))", + "lambda_format": "PySRFunction(X=>(Cd_tot - (Cd_rear + 10.025474))**2*(daT_dt*0.0035464468 - 1*(-0.04003622)))" + } + ] + }, + "karman_re100": { + "best_sympy": "mu_u_a - (daF_dt + daT_dt*(-0.45103812)) + 4.084246", + "best_score": 0.9025843203104927, + "equations": [ + { + "complexity": 1, + "loss": 12.876947, + "equation": "bias", + "score": 0.0, + "sympy_format": "bias", + "lambda_format": "PySRFunction(X=>bias)" + }, + { + "complexity": 2, + "loss": 3.1669734, + "equation": "4.116079", + "score": 1.4026622900123324, + "sympy_format": "4.11607900000000", + "lambda_format": "PySRFunction(X=>4.11607900000000)" + }, + { + "complexity": 4, + "loss": 1.2881802, + "equation": "4.1220803 - daF_dt", + "score": 0.44977292183846773, + "sympy_format": "4.1220803 - daF_dt", + "lambda_format": "PySRFunction(X=>4.1220803 - daF_dt)" + }, + { + "complexity": 6, + "loss": 1.2329304, + "equation": "(mu_Cl_diff * -23.485401) - daF_dt", + "score": 0.021918374913999877, + "sympy_format": "-daF_dt + mu_Cl_diff*(-23.485401)", + "lambda_format": "PySRFunction(X=>-daF_dt + mu_Cl_diff*(-23.485401))" + }, + { + "complexity": 7, + "loss": 1.1218283, + "equation": "square(mu_v_a + 2.06478) - daF_dt", + "score": 0.09443400981326935, + "sympy_format": "-daF_dt + (mu_v_a + 2.06478)**2", + "lambda_format": "PySRFunction(X=>-daF_dt + (mu_v_a + 2.06478)**2)" + }, + { + "complexity": 8, + "loss": 0.7498808, + "equation": "(4.0875216 - daF_dt) - (mu_Cl_diff * daT_dt)", + "score": 0.4028007835034975, + "sympy_format": "-daF_dt - daT_dt*mu_Cl_diff + 4.0875216", + "lambda_format": "PySRFunction(X=>-daF_dt - daT_dt*mu_Cl_diff + 4.0875216)" + }, + { + "complexity": 9, + "loss": 0.51422507, + "equation": "4.1056037 - (daF_dt - (daT_dt * 0.39707044))", + "score": 0.37725321157438296, + "sympy_format": "4.1056037 - (daF_dt - 0.39707044*daT_dt)", + "lambda_format": "PySRFunction(X=>4.1056037 - (daF_dt - 0.39707044*daT_dt))" + }, + { + "complexity": 11, + "loss": 0.30851284, + "equation": "(mu_u_a - ((daT_dt * -0.45103812) + daF_dt)) + 4.084246", + "score": 0.2554487928832665, + "sympy_format": "mu_u_a - (daF_dt + daT_dt*(-0.45103812)) + 4.084246", + "lambda_format": "PySRFunction(X=>mu_u_a - (daF_dt + daT_dt*(-0.45103812)) + 4.084246)" + }, + { + "complexity": 13, + "loss": 0.2730008, + "equation": "((mu_u_a + ((daT_dt + mu_u_a) * 0.48033392)) - daF_dt) + 4.0738463", + "score": 0.06114436882063494, + "sympy_format": "-daF_dt + mu_u_a + (daT_dt + mu_u_a)*0.48033392 + 4.0738463", + "lambda_format": "PySRFunction(X=>-daF_dt + mu_u_a + (daT_dt + mu_u_a)*0.48033392 + 4.0738463)" + }, + { + "complexity": 14, + "loss": 0.25823805, + "equation": "((daT_dt + ((mu_u_a * 4.05885) - daF_dt)) * 0.60417837) + 4.047715", + "score": 0.05559289164576399, + "sympy_format": "(-daF_dt + daT_dt + mu_u_a*4.05885)*0.60417837 + 4.047715", + "lambda_format": "PySRFunction(X=>(-daF_dt + daT_dt + mu_u_a*4.05885)*0.60417837 + 4.047715)" + }, + { + "complexity": 15, + "loss": 0.22480169, + "equation": "((daT_dt * 0.4729197) + 4.112852) + ((mu_Cl_diff * (mu_u_a * Cl_diff)) - daF_dt)", + "score": 0.13866319815296071, + "sympy_format": "Cl_diff*mu_Cl_diff*mu_u_a - daF_dt + daT_dt*0.4729197 + 4.112852", + "lambda_format": "PySRFunction(X=>Cl_diff*mu_Cl_diff*mu_u_a - daF_dt + daT_dt*0.4729197 + 4.112852)" + } + ] + }, + "karman_re200": { + "best_sympy": "-daB_dt*mu_u_a + daF_dt/(-0.77701634) + 2.6764865", + "best_score": 0.7626451489320526, + "equations": [ + { + "complexity": 1, + "loss": 23.63484, + "equation": "bias", + "score": 0.0, + "sympy_format": "bias", + "lambda_format": "PySRFunction(X=>bias)" + }, + { + "complexity": 2, + "loss": 18.926178, + "equation": "3.1692464", + "score": 0.22217585150789548, + "sympy_format": "3.16924640000000", + "lambda_format": "PySRFunction(X=>3.16924640000000)" + }, + { + "complexity": 3, + "loss": 10.618896, + "equation": "bias - daF_dt", + "score": 0.5779109874852081, + "sympy_format": "bias - daF_dt", + "lambda_format": "PySRFunction(X=>bias - daF_dt)" + }, + { + "complexity": 4, + "loss": 5.964711, + "equation": "3.1573398 - daF_dt", + "score": 0.5767744505191935, + "sympy_format": "3.1573398 - daF_dt", + "lambda_format": "PySRFunction(X=>3.1573398 - daF_dt)" + }, + { + "complexity": 6, + "loss": 5.523261, + "equation": "(Cl_diff * -0.32696325) - daF_dt", + "score": 0.038446079180301636, + "sympy_format": "Cl_diff*(-0.32696325) - daF_dt", + "lambda_format": "PySRFunction(X=>Cl_diff*(-0.32696325) - daF_dt)" + }, + { + "complexity": 7, + "loss": 5.166977, + "equation": "(2.39424 - daF_dt) / 0.7592664", + "score": 0.0666806487586416, + "sympy_format": "(2.39424 - daF_dt)/0.7592664", + "lambda_format": "PySRFunction(X=>(2.39424 - daF_dt)/0.7592664)" + }, + { + "complexity": 8, + "loss": 5.116335, + "equation": "((daF_dt + Cl_diff) * -0.3145073) - daF_dt", + "score": 0.009849435552190337, + "sympy_format": "-daF_dt + (Cl_diff + daF_dt)*(-0.3145073)", + "lambda_format": "PySRFunction(X=>-daF_dt + (Cl_diff + daF_dt)*(-0.3145073))" + }, + { + "complexity": 9, + "loss": 4.9789214, + "equation": "(2.388666 - daF_dt) / (0.7455222 - mu_Cl_tot)", + "score": 0.027225081184258832, + "sympy_format": "(2.388666 - daF_dt)/(0.7455222 - mu_Cl_tot)", + "lambda_format": "PySRFunction(X=>(2.388666 - daF_dt)/(0.7455222 - mu_Cl_tot))" + }, + { + "complexity": 10, + "loss": 4.968581, + "equation": "((daF_dt + Cl_diff) * -0.22730531) - (daF_dt - bias)", + "score": 0.002078994969522485, + "sympy_format": "(Cl_diff + daF_dt)*(-0.22730531) - (-bias + daF_dt)", + "lambda_format": "PySRFunction(X=>(Cl_diff + daF_dt)*(-0.22730531) - (-bias + daF_dt))" + }, + { + "complexity": 11, + "loss": 4.492221, + "equation": "((daF_dt / -0.77701634) - (mu_u_a * daB_dt)) + 2.6764865", + "score": 0.10078705202221648, + "sympy_format": "-daB_dt*mu_u_a + daF_dt/(-0.77701634) + 2.6764865", + "lambda_format": "PySRFunction(X=>-daB_dt*mu_u_a + daF_dt/(-0.77701634) + 2.6764865)" + }, + { + "complexity": 12, + "loss": 4.0992255, + "equation": "(3.1383474 - (daF_dt * 1.742641)) - (daB_dt * 0.3137331)", + "score": 0.09154918081430663, + "sympy_format": "-0.3137331*daB_dt - 1.742641*daF_dt + 3.1383474", + "lambda_format": "PySRFunction(X=>-0.3137331*daB_dt - 1.742641*daF_dt + 3.1383474)" + }, + { + "complexity": 14, + "loss": 3.4891856, + "equation": "(((daF_dt + -1.7668675) / (mu_Cl_tot + -0.17951706)) - daB_dt) * 0.32972044", + "score": 0.08056484843951753, + "sympy_format": "(-daB_dt + (daF_dt - 1.7668675)/(mu_Cl_tot - 0.17951706))*0.32972044", + "lambda_format": "PySRFunction(X=>(-daB_dt + (daF_dt - 1.7668675)/(mu_Cl_tot - 0.17951706))*0.32972044)" + } + ] + }, + "karman_re400": { + "best_sympy": "-daF_dt + u_a*(-0.073740624) + 2.584631", + "best_score": 0.8037011730215763, + "equations": [ + { + "complexity": 1, + "loss": 12.389752, + "equation": "bias", + "score": 0.0, + "sympy_format": "bias", + "lambda_format": "PySRFunction(X=>bias)" + }, + { + "complexity": 2, + "loss": 9.910323, + "equation": "2.574628", + "score": 0.22329273814786962, + "sympy_format": "2.57462800000000", + "lambda_format": "PySRFunction(X=>2.57462800000000)" + }, + { + "complexity": 3, + "loss": 6.7695775, + "equation": "bias - daF_dt", + "score": 0.3811382638550079, + "sympy_format": "bias - daF_dt", + "lambda_format": "PySRFunction(X=>bias - daF_dt)" + }, + { + "complexity": 4, + "loss": 4.2807117, + "equation": "2.5775137 - daF_dt", + "score": 0.45831939647918934, + "sympy_format": "2.5775137 - daF_dt", + "lambda_format": "PySRFunction(X=>2.5775137 - daF_dt)" + }, + { + "complexity": 6, + "loss": 2.597043, + "equation": "(2.5808449 - daF_dt) - daF_dt", + "score": 0.24987289535332466, + "sympy_format": "-daF_dt - daF_dt + 2.5808449", + "lambda_format": "PySRFunction(X=>-daF_dt - daF_dt + 2.5808449)" + }, + { + "complexity": 7, + "loss": 2.5864377, + "equation": "(daF_dt * -1.9266839) + 2.580388", + "score": 0.004091966542131783, + "sympy_format": "2.580388 + daF_dt*(-1.9266839)", + "lambda_format": "PySRFunction(X=>2.580388 + daF_dt*(-1.9266839))" + }, + { + "complexity": 8, + "loss": 2.442531, + "equation": "2.5810947 - (daF_dt + (daF_dt + mu_u_a))", + "score": 0.05724672679954597, + "sympy_format": "2.5810947 - (daF_dt + daF_dt + mu_u_a)", + "lambda_format": "PySRFunction(X=>2.5810947 - (daF_dt + daF_dt + mu_u_a))" + }, + { + "complexity": 9, + "loss": 1.9453849, + "equation": "((u_a * -0.073740624) - daF_dt) + 2.584631", + "score": 0.2275749472450687, + "sympy_format": "-daF_dt + u_a*(-0.073740624) + 2.584631", + "lambda_format": "PySRFunction(X=>-daF_dt + u_a*(-0.073740624) + 2.584631)" + }, + { + "complexity": 11, + "loss": 1.6397182, + "equation": "(((u_a - daT_dt) * -0.07628765) + 2.5876338) - daF_dt", + "score": 0.08546772585986931, + "sympy_format": "-daF_dt + (-daT_dt + u_a)*(-0.07628765) + 2.5876338", + "lambda_format": "PySRFunction(X=>-daF_dt + (-daT_dt + u_a)*(-0.07628765) + 2.5876338)" + }, + { + "complexity": 12, + "loss": 1.5935242, + "equation": "((daF_dt - (mu_u_a * -7.5331697)) * -1.4945722) + 2.5844433", + "score": 0.028576356347584618, + "sympy_format": "2.5844433 + (daF_dt - (-7.5331697)*mu_u_a)*(-1.4945722)", + "lambda_format": "PySRFunction(X=>2.5844433 + (daF_dt - (-7.5331697)*mu_u_a)*(-1.4945722))" + }, + { + "complexity": 13, + "loss": 1.465523, + "equation": "((((u_a - daT_dt) + Cl_tot) * -0.073437564) - daF_dt) + 2.5909092", + "score": 0.08373586610794806, + "sympy_format": "-daF_dt + (Cl_tot - daT_dt + u_a)*(-0.073437564) + 2.5909092", + "lambda_format": "PySRFunction(X=>-daF_dt + (Cl_tot - daT_dt + u_a)*(-0.073437564) + 2.5909092)" + }, + { + "complexity": 14, + "loss": 1.4531459, + "equation": "((u_a - (daT_dt * 2.7233648)) * -0.07121675) + (2.5913422 - daF_dt)", + "score": 0.008481382862090062, + "sympy_format": "-daF_dt + (-2.7233648*daT_dt + u_a)*(-0.07121675) + 2.5913422", + "lambda_format": "PySRFunction(X=>-daF_dt + (-2.7233648*daT_dt + u_a)*(-0.07121675) + 2.5913422)" + }, + { + "complexity": 15, + "loss": 1.3641217, + "equation": "((Cl_tot + ((u_a - daT_dt) - daT_dt)) * -0.0706468) + (2.5929654 - daF_dt)", + "score": 0.06322001417080474, + "sympy_format": "-daF_dt + (Cl_tot - daT_dt - daT_dt + u_a)*(-0.0706468) + 2.5929654", + "lambda_format": "PySRFunction(X=>-daF_dt + (Cl_tot - daT_dt - daT_dt + u_a)*(-0.0706468) + 2.5929654)" + } + ] + } +} \ No newline at end of file diff --git a/src/SR_analysis/validate/results/archive/pysr_deep_individual_deep.json b/src/SR_analysis/validate/results/archive/pysr_deep_individual_deep.json new file mode 100644 index 0000000..23cf423 --- /dev/null +++ b/src/SR_analysis/validate/results/archive/pysr_deep_individual_deep.json @@ -0,0 +1,410 @@ +{ + "karman_re50": { + "front": { + "best_sympy": "Cd_rear*Cd_tot - 0.5113535*Cl_tot + mu_u_a*(2.8397622 - daB_dt)", + "best_score": 0.9273744572708317, + "equations": [ + { + "complexity": 1, + "loss": 2.9416304, + "equation": "Cd_rear", + "score": 0.0, + "sympy_format": "Cd_rear", + "lambda_format": "PySRFunction(X=>Cd_rear)" + }, + { + "complexity": 3, + "loss": 0.8881686, + "equation": "mu_Cl_tot - Cl_tot", + "score": 0.5987788373052547, + "sympy_format": "-Cl_tot + mu_Cl_tot", + "lambda_format": "PySRFunction(X=>-Cl_tot + mu_Cl_tot)" + }, + { + "complexity": 4, + "loss": 0.79536694, + "equation": "mu_Cl_tot * -20.556822", + "score": 0.11035802191815856, + "sympy_format": "mu_Cl_tot*(-20.556822)", + "lambda_format": "PySRFunction(X=>mu_Cl_tot*(-20.556822))" + }, + { + "complexity": 5, + "loss": 0.75886726, + "equation": "(mu_Cl_tot * Cl_tot) - Cl_tot", + "score": 0.04697669382642563, + "sympy_format": "Cl_tot*mu_Cl_tot - Cl_tot", + "lambda_format": "PySRFunction(X=>Cl_tot*mu_Cl_tot - Cl_tot)" + }, + { + "complexity": 6, + "loss": 0.49224868, + "equation": "(Cd_tot + Cl_tot) * -0.726867", + "score": 0.43284283813900465, + "sympy_format": "(Cd_tot + Cl_tot)*(-0.726867)", + "lambda_format": "PySRFunction(X=>(Cd_tot + Cl_tot)*(-0.726867))" + }, + { + "complexity": 8, + "loss": 0.39933395, + "equation": "mu_u_a - ((Cd_tot + Cl_tot) * 0.6607092)", + "score": 0.10459300085245019, + "sympy_format": "mu_u_a - 0.6607092*(Cd_tot + Cl_tot)", + "lambda_format": "PySRFunction(X=>mu_u_a - 0.6607092*(Cd_tot + Cl_tot))" + }, + { + "complexity": 10, + "loss": 0.35132024, + "equation": "(mu_u_a + mu_u_a) - ((Cl_tot + Cd_tot) * 0.5945518)", + "score": 0.06404993102996497, + "sympy_format": "mu_u_a + mu_u_a - 0.5945518*(Cd_tot + Cl_tot)", + "lambda_format": "PySRFunction(X=>mu_u_a + mu_u_a - 0.5945518*(Cd_tot + Cl_tot))" + }, + { + "complexity": 11, + "loss": 0.34404334, + "equation": "(mu_u_a * 2.5699782) - ((Cl_tot + Cd_tot) * 0.5568322)", + "score": 0.020930534376940382, + "sympy_format": "mu_u_a*2.5699782 - 0.5568322*(Cd_tot + Cl_tot)", + "lambda_format": "PySRFunction(X=>mu_u_a*2.5699782 - 0.5568322*(Cd_tot + Cl_tot))" + }, + { + "complexity": 12, + "loss": 0.31634966, + "equation": "(((Cd_tot * Cd_rear) + mu_u_a) + mu_u_a) - (Cl_tot * 0.62182605)", + "score": 0.08391951697627671, + "sympy_format": "Cd_rear*Cd_tot - 0.62182605*Cl_tot + mu_u_a + mu_u_a", + "lambda_format": "PySRFunction(X=>Cd_rear*Cd_tot - 0.62182605*Cl_tot + mu_u_a + mu_u_a)" + }, + { + "complexity": 13, + "loss": 0.29955482, + "equation": "((Cd_rear * Cd_tot) + (mu_u_a * 2.802526)) - (Cl_tot * 0.5629972)", + "score": 0.0545506816299969, + "sympy_format": "Cd_rear*Cd_tot - 0.5629972*Cl_tot + mu_u_a*2.802526", + "lambda_format": "PySRFunction(X=>Cd_rear*Cd_tot - 0.5629972*Cl_tot + mu_u_a*2.802526)" + }, + { + "complexity": 14, + "loss": 0.2993588, + "equation": "(((Cd_tot * (Cd_rear + mu_v_a)) + mu_u_a) + mu_u_a) - (Cl_tot * 0.61686575)", + "score": 0.0006545852371808462, + "sympy_format": "Cd_tot*(Cd_rear + mu_v_a) - 0.61686575*Cl_tot + mu_u_a + mu_u_a", + "lambda_format": "PySRFunction(X=>Cd_tot*(Cd_rear + mu_v_a) - 0.61686575*Cl_tot + mu_u_a + mu_u_a)" + }, + { + "complexity": 15, + "loss": 0.24793048, + "equation": "(Cd_tot * Cd_rear) + (((2.8397622 - daB_dt) * mu_u_a) - (Cl_tot * 0.5113535))", + "score": 0.18849446967955016, + "sympy_format": "Cd_rear*Cd_tot - 0.5113535*Cl_tot + mu_u_a*(2.8397622 - daB_dt)", + "lambda_format": "PySRFunction(X=>Cd_rear*Cd_tot - 0.5113535*Cl_tot + mu_u_a*(2.8397622 - daB_dt))" + } + ] + } + }, + "karman_re100": { + "front": { + "best_sympy": "mu_u_a*(-3.6490004) - (daF_dt - 1*2.3221624)/(Cl_diff - daF_dt)", + "best_score": 0.9763863955617698, + "equations": [ + { + "complexity": 1, + "loss": 1.8402938, + "equation": "daF_dt", + "score": 0.0, + "sympy_format": "daF_dt", + "lambda_format": "PySRFunction(X=>daF_dt)" + }, + { + "complexity": 2, + "loss": 1.830206, + "equation": "-0.3915825", + "score": 0.005496703963411108, + "sympy_format": "-0.391582500000000", + "lambda_format": "PySRFunction(X=>-0.391582500000000)" + }, + { + "complexity": 3, + "loss": 0.52802104, + "equation": "Cd_rear * mu_u_a", + "score": 1.2430476764089609, + "sympy_format": "Cd_rear*mu_u_a", + "lambda_format": "PySRFunction(X=>Cd_rear*mu_u_a)" + }, + { + "complexity": 4, + "loss": 0.23401318, + "equation": "mu_u_a * -4.109215", + "score": 0.8137586928393354, + "sympy_format": "mu_u_a*(-4.109215)", + "lambda_format": "PySRFunction(X=>mu_u_a*(-4.109215))" + }, + { + "complexity": 5, + "loss": 0.20410247, + "equation": "mu_u_a / (mu_v_a + mu_Cl_diff)", + "score": 0.13675526690444537, + "sympy_format": "mu_u_a/(mu_Cl_diff + mu_v_a)", + "lambda_format": "PySRFunction(X=>mu_u_a/(mu_Cl_diff + mu_v_a))" + }, + { + "complexity": 6, + "loss": 0.14800628, + "equation": "(mu_u_a / mu_Cl_diff) * 0.6710667", + "score": 0.32136746635708235, + "sympy_format": "mu_u_a*0.6710667/mu_Cl_diff", + "lambda_format": "PySRFunction(X=>mu_u_a*0.6710667/mu_Cl_diff)" + }, + { + "complexity": 7, + "loss": 0.1357354, + "equation": "(mu_u_a * -4.051503) - 0.3140858", + "score": 0.08654730291082482, + "sympy_format": "mu_u_a*(-4.051503) - 1*0.3140858", + "lambda_format": "PySRFunction(X=>mu_u_a*(-4.051503) - 1*0.3140858)" + }, + { + "complexity": 8, + "loss": 0.09827535, + "equation": "mu_Cl_diff + (mu_u_a * (0.66638017 / mu_Cl_diff))", + "score": 0.3229341696491608, + "sympy_format": "mu_Cl_diff + mu_u_a*0.66638017/mu_Cl_diff", + "lambda_format": "PySRFunction(X=>mu_Cl_diff + mu_u_a*0.66638017/mu_Cl_diff)" + }, + { + "complexity": 9, + "loss": 0.074505165, + "equation": "(square(daF_dt) - (mu_u_a * -31.115244)) / Cl_diff", + "score": 0.27690478089489157, + "sympy_format": "(daF_dt**2 - (-31.115244)*mu_u_a)/Cl_diff", + "lambda_format": "PySRFunction(X=>(daF_dt**2 - (-31.115244)*mu_u_a)/Cl_diff)" + }, + { + "complexity": 11, + "loss": 0.05921805, + "equation": "(mu_u_a * -3.7892919) - ((daF_dt - 2.707404) / Cl_diff)", + "score": 0.11482102888748913, + "sympy_format": "mu_u_a*(-3.7892919) - (daF_dt - 1*2.707404)/Cl_diff", + "lambda_format": "PySRFunction(X=>mu_u_a*(-3.7892919) - (daF_dt - 1*2.707404)/Cl_diff)" + }, + { + "complexity": 12, + "loss": 0.056852993, + "equation": "(daF_dt * 0.23742867) + ((mu_u_a * -3.7551446) - 0.32114586)", + "score": 0.04075752793823579, + "sympy_format": "daF_dt*0.23742867 + mu_u_a*(-3.7551446) - 1*0.32114586", + "lambda_format": "PySRFunction(X=>daF_dt*0.23742867 + mu_u_a*(-3.7551446) - 1*0.32114586)" + }, + { + "complexity": 13, + "loss": 0.04321776, + "equation": "(mu_u_a * -3.6490004) - ((daF_dt - 2.3221624) / (Cl_diff - daF_dt))", + "score": 0.2742173442500691, + "sympy_format": "mu_u_a*(-3.6490004) - (daF_dt - 1*2.3221624)/(Cl_diff - daF_dt)", + "lambda_format": "PySRFunction(X=>mu_u_a*(-3.6490004) - (daF_dt - 1*2.3221624)/(Cl_diff - daF_dt))" + }, + { + "complexity": 15, + "loss": 0.042888116, + "equation": "(mu_u_a * -3.6171958) - square(0.47372434 - ((daF_dt / Cl_diff) * -1.7419438))", + "score": 0.003828375318654601, + "sympy_format": "mu_u_a*(-3.6171958) - (0.47372434 - (-1.7419438)*daF_dt/Cl_diff)**2", + "lambda_format": "PySRFunction(X=>mu_u_a*(-3.6171958) - (0.47372434 - (-1.7419438)*daF_dt/Cl_diff)**2)" + } + ] + } + }, + "karman_re200": { + "front": { + "best_sympy": "daF_dt - (-0.35635605)*(daB_dt + u_a/Cl_diff)", + "best_score": 0.8101764468157443, + "equations": [ + { + "complexity": 1, + "loss": 5.4920754, + "equation": "mu_Cd_tot", + "score": 0.0, + "sympy_format": "mu_Cd_tot", + "lambda_format": "PySRFunction(X=>mu_Cd_tot)" + }, + { + "complexity": 2, + "loss": 5.2705092, + "equation": "0.48291793", + "score": 0.04117923659541323, + "sympy_format": "0.482917930000000", + "lambda_format": "PySRFunction(X=>0.482917930000000)" + }, + { + "complexity": 3, + "loss": 4.888712, + "equation": "mu_u_a * Cd_rear", + "score": 0.07519810616723817, + "sympy_format": "Cd_rear*mu_u_a", + "lambda_format": "PySRFunction(X=>Cd_rear*mu_u_a)" + }, + { + "complexity": 4, + "loss": 3.9153118, + "equation": "daF_dt * 0.4475457", + "score": 0.22203390543678164, + "sympy_format": "daF_dt*0.4475457", + "lambda_format": "PySRFunction(X=>daF_dt*0.4475457)" + }, + { + "complexity": 5, + "loss": 2.7710476, + "equation": "daF_dt + (mu_u_a * Cl_diff)", + "score": 0.34566952502508724, + "sympy_format": "Cl_diff*mu_u_a + daF_dt", + "lambda_format": "PySRFunction(X=>Cl_diff*mu_u_a + daF_dt)" + }, + { + "complexity": 6, + "loss": 2.2958577, + "equation": "daF_dt + (daB_dt * 0.3985408)", + "score": 0.18811894446268496, + "sympy_format": "daB_dt*0.3985408 + daF_dt", + "lambda_format": "PySRFunction(X=>daB_dt*0.3985408 + daF_dt)" + }, + { + "complexity": 8, + "loss": 1.8868104, + "equation": "daF_dt - (mu_u_a / (0.11357432 - mu_Cd_tot))", + "score": 0.09810935739699866, + "sympy_format": "daF_dt - mu_u_a/(0.11357432 - mu_Cd_tot)", + "lambda_format": "PySRFunction(X=>daF_dt - mu_u_a/(0.11357432 - mu_Cd_tot))" + }, + { + "complexity": 9, + "loss": 1.6995139, + "equation": "(daF_dt * 0.66847146) + (mu_u_a * -8.630447)", + "score": 0.10454551539400395, + "sympy_format": "daF_dt*0.66847146 + mu_u_a*(-8.630447)", + "lambda_format": "PySRFunction(X=>daF_dt*0.66847146 + mu_u_a*(-8.630447))" + }, + { + "complexity": 10, + "loss": 1.000467, + "equation": "daF_dt - ((daB_dt + (u_a / Cl_diff)) * -0.35635605)", + "score": 0.5298753780072896, + "sympy_format": "daF_dt - (-0.35635605)*(daB_dt + u_a/Cl_diff)", + "lambda_format": "PySRFunction(X=>daF_dt - (-0.35635605)*(daB_dt + u_a/Cl_diff))" + }, + { + "complexity": 12, + "loss": 0.8764493, + "equation": "daF_dt - ((daB_dt + ((Cl_diff + u_a) / Cl_diff)) * -0.35635605)", + "score": 0.06617165548990826, + "sympy_format": "daF_dt - (-0.35635605)*(daB_dt + (Cl_diff + u_a)/Cl_diff)", + "lambda_format": "PySRFunction(X=>daF_dt - (-0.35635605)*(daB_dt + (Cl_diff + u_a)/Cl_diff))" + }, + { + "complexity": 13, + "loss": 0.8759513, + "equation": "(daF_dt - ((daB_dt + (u_a / Cl_diff)) * -0.3529676)) - -0.35354236", + "score": 0.000568363208054163, + "sympy_format": "daF_dt - (-0.3529676)*(daB_dt + u_a/Cl_diff) - 1*(-0.35354236)", + "lambda_format": "PySRFunction(X=>daF_dt - (-0.3529676)*(daB_dt + u_a/Cl_diff) - 1*(-0.35354236))" + }, + { + "complexity": 14, + "loss": 0.7620857, + "equation": "(daF_dt - ((daB_dt + (u_a / Cl_diff)) * -0.36135)) - (mu_Cl_diff * Cd_tot)", + "score": 0.13925147922941675, + "sympy_format": "-Cd_tot*mu_Cl_diff + daF_dt - (-0.36135)*(daB_dt + u_a/Cl_diff)", + "lambda_format": "PySRFunction(X=>-Cd_tot*mu_Cl_diff + daF_dt - (-0.36135)*(daB_dt + u_a/Cl_diff))" + }, + { + "complexity": 15, + "loss": 0.7547256, + "equation": "daF_dt - (((mu_u_a + mu_v_a) + (daB_dt * -0.03457546)) / (0.1620542 - mu_Cd_tot))", + "score": 0.009704777075542995, + "sympy_format": "daF_dt - (daB_dt*(-0.03457546) + mu_u_a + mu_v_a)/(0.1620542 - mu_Cd_tot)", + "lambda_format": "PySRFunction(X=>daF_dt - (daB_dt*(-0.03457546) + mu_u_a + mu_v_a)/(0.1620542 - mu_Cd_tot))" + } + ] + } + }, + "karman_re400": { + "front": { + "best_sympy": "mu_Cl_tot*21.972973 + mu_u_a*(Cl_diff - 1*(-18.038433))", + "best_score": 0.8572896229052032, + "equations": [ + { + "complexity": 1, + "loss": 1.308247, + "equation": "mu_u_a", + "score": 0.0, + "sympy_format": "mu_u_a", + "lambda_format": "PySRFunction(X=>mu_u_a)" + }, + { + "complexity": 3, + "loss": 1.0834358, + "equation": "u_a / u_m", + "score": 0.09427539265589109, + "sympy_format": "u_a/u_m", + "lambda_format": "PySRFunction(X=>u_a/u_m)" + }, + { + "complexity": 4, + "loss": 0.34880155, + "equation": "mu_u_a * 10.45298", + "score": 1.133389430965925, + "sympy_format": "mu_u_a*10.45298", + "lambda_format": "PySRFunction(X=>mu_u_a*10.45298)" + }, + { + "complexity": 6, + "loss": 0.2902321, + "equation": "(mu_Cl_tot + mu_u_a) * 10.068496", + "score": 0.09191109406466053, + "sympy_format": "(mu_Cl_tot + mu_u_a)*10.068496", + "lambda_format": "PySRFunction(X=>(mu_Cl_tot + mu_u_a)*10.068496)" + }, + { + "complexity": 8, + "loss": 0.2603338, + "equation": "(Cl_diff - -19.014246) * (mu_u_a + mu_Cl_tot)", + "score": 0.054358146979688035, + "sympy_format": "(Cl_diff - 1*(-19.014246))*(mu_Cl_tot + mu_u_a)", + "lambda_format": "PySRFunction(X=>(Cl_diff - 1*(-19.014246))*(mu_Cl_tot + mu_u_a))" + }, + { + "complexity": 10, + "loss": 0.23128638, + "equation": "(mu_u_a + (Cl_tot / u_m)) * (Cl_diff - -18.46042)", + "score": 0.05915398540273152, + "sympy_format": "(Cl_diff - 1*(-18.46042))*(Cl_tot/u_m + mu_u_a)", + "lambda_format": "PySRFunction(X=>(Cl_diff - 1*(-18.46042))*(Cl_tot/u_m + mu_u_a))" + }, + { + "complexity": 11, + "loss": 0.21716471, + "equation": "(mu_u_a * (Cl_diff - -18.038433)) + (mu_Cl_tot * 21.972973)", + "score": 0.06300058505890627, + "sympy_format": "mu_Cl_tot*21.972973 + mu_u_a*(Cl_diff - 1*(-18.038433))", + "lambda_format": "PySRFunction(X=>mu_Cl_tot*21.972973 + mu_u_a*(Cl_diff - 1*(-18.038433)))" + }, + { + "complexity": 13, + "loss": 0.2064088, + "equation": "((Cl_diff - -17.974705) * mu_u_a) + ((mu_Cl_tot * 21.927103) - mu_v_a)", + "score": 0.02539871476088764, + "sympy_format": "mu_Cl_tot*21.927103 + mu_u_a*(Cl_diff - 1*(-17.974705)) - mu_v_a", + "lambda_format": "PySRFunction(X=>mu_Cl_tot*21.927103 + mu_u_a*(Cl_diff - 1*(-17.974705)) - mu_v_a)" + }, + { + "complexity": 15, + "loss": 0.18927771, + "equation": "(mu_Cl_tot * 28.493284) + (((Cl_diff - -17.826105) * mu_u_a) - (daT_dt * mu_Cl_diff))", + "score": 0.04332168333950189, + "sympy_format": "-daT_dt*mu_Cl_diff + mu_Cl_tot*28.493284 + mu_u_a*(Cl_diff - 1*(-17.826105))", + "lambda_format": "PySRFunction(X=>-daT_dt*mu_Cl_diff + mu_Cl_tot*28.493284 + mu_u_a*(Cl_diff - 1*(-17.826105)))" + } + ] + } + } +} \ No newline at end of file diff --git a/src/SR_analysis/validate/results/archive/pysr_deep_joint_deep.json b/src/SR_analysis/validate/results/archive/pysr_deep_joint_deep.json new file mode 100644 index 0000000..313a578 --- /dev/null +++ b/src/SR_analysis/validate/results/archive/pysr_deep_joint_deep.json @@ -0,0 +1,94 @@ +{ + "best_sympy": "daB_dt*0.38504666 + daF_dt + mu_Cl_tot*(-14.951645)", + "best_score": 0.6100499772229617, + "equations": [ + { + "complexity": 1, + "loss": 3.279333, + "equation": "mu_Cl_diff", + "score": 0.0, + "sympy_format": "mu_Cl_diff", + "lambda_format": "PySRFunction(X=>mu_Cl_diff)" + }, + { + "complexity": 3, + "loss": 2.955941, + "equation": "Cl_diff * mu_Cl_tot", + "score": 0.051911502288096975, + "sympy_format": "Cl_diff*mu_Cl_tot", + "lambda_format": "PySRFunction(X=>Cl_diff*mu_Cl_tot)" + }, + { + "complexity": 4, + "loss": 2.5313113, + "equation": "mu_Cl_tot * -18.69284", + "score": 0.1550795746187338, + "sympy_format": "mu_Cl_tot*(-18.69284)", + "lambda_format": "PySRFunction(X=>mu_Cl_tot*(-18.69284))" + }, + { + "complexity": 5, + "loss": 2.4476228, + "equation": "(mu_Cl_tot * u_c) / Cl_diff", + "score": 0.033620201052977965, + "sympy_format": "mu_Cl_tot*u_c/Cl_diff", + "lambda_format": "PySRFunction(X=>mu_Cl_tot*u_c/Cl_diff)" + }, + { + "complexity": 6, + "loss": 1.862076, + "equation": "daF_dt + (daB_dt * 0.39345038)", + "score": 0.27342527346200884, + "sympy_format": "daB_dt*0.39345038 + daF_dt", + "lambda_format": "PySRFunction(X=>daB_dt*0.39345038 + daF_dt)" + }, + { + "complexity": 8, + "loss": 1.7427546, + "equation": "(mu_Cl_diff - (daB_dt * -0.39289773)) + daF_dt", + "score": 0.03311251473051387, + "sympy_format": "-(-0.39289773)*daB_dt + daF_dt + mu_Cl_diff", + "lambda_format": "PySRFunction(X=>-(-0.39289773)*daB_dt + daF_dt + mu_Cl_diff)" + }, + { + "complexity": 10, + "loss": 1.4857975, + "equation": "daF_dt + ((daB_dt * 0.38430518) + (mu_Cl_tot * u_a))", + "score": 0.07975764987424808, + "sympy_format": "daB_dt*0.38430518 + daF_dt + mu_Cl_tot*u_a", + "lambda_format": "PySRFunction(X=>daB_dt*0.38430518 + daF_dt + mu_Cl_tot*u_a)" + }, + { + "complexity": 11, + "loss": 1.3089999, + "equation": "((daB_dt * 0.38504666) + (mu_Cl_tot * -14.951645)) + daF_dt", + "score": 0.1266882546053524, + "sympy_format": "daB_dt*0.38504666 + daF_dt + mu_Cl_tot*(-14.951645)", + "lambda_format": "PySRFunction(X=>daB_dt*0.38504666 + daF_dt + mu_Cl_tot*(-14.951645))" + }, + { + "complexity": 12, + "loss": 1.1976256, + "equation": "((daB_dt * 0.38822716) + ((mu_Cl_tot * u_c) / Cl_diff)) + daF_dt", + "score": 0.0889224805532453, + "sympy_format": "daB_dt*0.38822716 + daF_dt + mu_Cl_tot*u_c/Cl_diff", + "lambda_format": "PySRFunction(X=>daB_dt*0.38822716 + daF_dt + mu_Cl_tot*u_c/Cl_diff)" + }, + { + "complexity": 13, + "loss": 1.1955373, + "equation": "daF_dt + ((daB_dt * 0.38732177) + ((mu_Cl_tot / Cl_diff) * 109.09013))", + "score": 0.0017452222162105428, + "sympy_format": "daB_dt*0.38732177 + daF_dt + mu_Cl_tot*109.09013/Cl_diff", + "lambda_format": "PySRFunction(X=>daB_dt*0.38732177 + daF_dt + mu_Cl_tot*109.09013/Cl_diff)" + }, + { + "complexity": 14, + "loss": 1.1628969, + "equation": "(((mu_Cl_tot * (u_c / Cl_diff)) + (daB_dt * 0.3716098)) + daF_dt) - mu_u_a", + "score": 0.027681488200222246, + "sympy_format": "daB_dt*0.3716098 + daF_dt - mu_u_a + mu_Cl_tot*u_c/Cl_diff", + "lambda_format": "PySRFunction(X=>daB_dt*0.3716098 + daF_dt - mu_u_a + mu_Cl_tot*u_c/Cl_diff)" + } + ] +} \ No newline at end of file diff --git a/src/SR_analysis/validate/results/archive/pysr_deep_joint_top.json b/src/SR_analysis/validate/results/archive/pysr_deep_joint_top.json new file mode 100644 index 0000000..4953622 --- /dev/null +++ b/src/SR_analysis/validate/results/archive/pysr_deep_joint_top.json @@ -0,0 +1,94 @@ +{ + "best_sympy": "3.41517 - daF_dt", + "best_score": 0.5265476719318889, + "equations": [ + { + "complexity": 1, + "loss": 14.187922, + "equation": "bias", + "score": 0.0, + "sympy_format": "bias", + "lambda_format": "PySRFunction(X=>bias)" + }, + { + "complexity": 2, + "loss": 8.361251, + "equation": "3.4138296", + "score": 0.5287829822428393, + "sympy_format": "3.41382960000000", + "lambda_format": "PySRFunction(X=>3.41382960000000)" + }, + { + "complexity": 4, + "loss": 3.9586535, + "equation": "3.41517 - daF_dt", + "score": 0.37385205742882277, + "sympy_format": "3.41517 - daF_dt", + "lambda_format": "PySRFunction(X=>3.41517 - daF_dt)" + }, + { + "complexity": 6, + "loss": 3.8633175, + "equation": "(3.3758485 - daF_dt) - mu_v_a", + "score": 0.012188835938836762, + "sympy_format": "-daF_dt - mu_v_a + 3.3758485", + "lambda_format": "PySRFunction(X=>-daF_dt - mu_v_a + 3.3758485)" + }, + { + "complexity": 7, + "loss": 3.7500641, + "equation": "square(1.8054985 - mu_v_a) - daF_dt", + "score": 0.029753337144933185, + "sympy_format": "-daF_dt + (1.8054985 - mu_v_a)**2", + "lambda_format": "PySRFunction(X=>-daF_dt + (1.8054985 - mu_v_a)**2)" + }, + { + "complexity": 8, + "loss": 3.625808, + "equation": "3.4007215 - ((mu_Cl_diff * daT_dt) + daF_dt)", + "score": 0.033695773148946735, + "sympy_format": "3.4007215 - (daF_dt + daT_dt*mu_Cl_diff)", + "lambda_format": "PySRFunction(X=>3.4007215 - (daF_dt + daT_dt*mu_Cl_diff))" + }, + { + "complexity": 9, + "loss": 3.160684, + "equation": "3.4405375 - ((daF_dt * -0.10720665) / mu_Cl_diff)", + "score": 0.13728870014874767, + "sympy_format": "3.4405375 - (-0.10720665)*daF_dt/mu_Cl_diff", + "lambda_format": "PySRFunction(X=>3.4405375 - (-0.10720665)*daF_dt/mu_Cl_diff)" + }, + { + "complexity": 11, + "loss": 2.8567598, + "equation": "4.4832644 - (((daF_dt + bias) * -0.102675565) / mu_Cl_diff)", + "score": 0.050550207180688596, + "sympy_format": "4.4832644 - (-0.102675565)*(bias + daF_dt)/mu_Cl_diff", + "lambda_format": "PySRFunction(X=>4.4832644 - (-0.102675565)*(bias + daF_dt)/mu_Cl_diff)" + }, + { + "complexity": 12, + "loss": 2.800737, + "equation": "4.202625 - (((daF_dt * -0.10674784) + -0.07497638) / mu_Cl_diff)", + "score": 0.019805448678422044, + "sympy_format": "4.202625 - (daF_dt*(-0.10674784) - 0.07497638)/mu_Cl_diff", + "lambda_format": "PySRFunction(X=>4.202625 - (daF_dt*(-0.10674784) - 0.07497638)/mu_Cl_diff)" + }, + { + "complexity": 14, + "loss": 2.750766, + "equation": "(mu_u_a - (((daF_dt + 0.6878209) / mu_Cl_diff) * -0.10825013)) + 4.2010245", + "score": 0.009001589242813583, + "sympy_format": "mu_u_a + 4.2010245 - (-0.10825013)*(daF_dt + 0.6878209)/mu_Cl_diff", + "lambda_format": "PySRFunction(X=>mu_u_a + 4.2010245 - (-0.10825013)*(daF_dt + 0.6878209)/mu_Cl_diff)" + }, + { + "complexity": 15, + "loss": 2.6732106, + "equation": "4.4728637 - (((daF_dt * -0.15587309) + -0.14546746) / (mu_Cl_diff - 0.029715132))", + "score": 0.028599196414474733, + "sympy_format": "4.4728637 - (daF_dt*(-0.15587309) - 0.14546746)/(mu_Cl_diff - 1*0.029715132)", + "lambda_format": "PySRFunction(X=>4.4728637 - (daF_dt*(-0.15587309) - 0.14546746)/(mu_Cl_diff - 1*0.029715132))" + } + ] +} \ No newline at end of file diff --git a/src/SR_analysis/validate/results/archive/pysr_illusion_deep_individual_deep.json b/src/SR_analysis/validate/results/archive/pysr_illusion_deep_individual_deep.json new file mode 100644 index 0000000..d3b9318 --- /dev/null +++ b/src/SR_analysis/validate/results/archive/pysr_illusion_deep_individual_deep.json @@ -0,0 +1,422 @@ +{ + "illusion_0.75L": { + "front": { + "best_sympy": "-(dCl_tot_dt + target_Cl)/5.8401647 - 1.2372783", + "best_score": 0.7232722304555419, + "equations": [ + { + "complexity": 1, + "loss": 1.6359515, + "equation": "Cl_err", + "score": 0.0, + "sympy_format": "Cl_err", + "lambda_format": "PySRFunction(X=>Cl_err)" + }, + { + "complexity": 2, + "loss": 0.075797155, + "equation": "-1.2395153", + "score": 3.071919112284068, + "sympy_format": "-1.23951530000000", + "lambda_format": "PySRFunction(X=>-1.23951530000000)" + }, + { + "complexity": 4, + "loss": 0.057897076, + "equation": "Cl_err - 1.2562019", + "score": 0.13469693826111778, + "sympy_format": "Cl_err - 1*1.2562019", + "lambda_format": "PySRFunction(X=>Cl_err - 1*1.2562019)" + }, + { + "complexity": 6, + "loss": 0.037532225, + "equation": "-1.2431892 - (dCl_tot_dt / target_Cd)", + "score": 0.2167334925729904, + "sympy_format": "-dCl_tot_dt/target_Cd - 1.2431892", + "lambda_format": "PySRFunction(X=>-dCl_tot_dt/target_Cd - 1.2431892)" + }, + { + "complexity": 8, + "loss": 0.026256593, + "equation": "-1.2326334 - ((Cl_tot + dCl_tot_dt) / target_Cd)", + "score": 0.1786413889438378, + "sympy_format": "-1.2326334 - (Cl_tot + dCl_tot_dt)/target_Cd", + "lambda_format": "PySRFunction(X=>-1.2326334 - (Cl_tot + dCl_tot_dt)/target_Cd)" + }, + { + "complexity": 9, + "loss": 0.020975176, + "equation": "-1.2372783 - ((target_Cl + dCl_tot_dt) / 5.8401647)", + "score": 0.22457747614686055, + "sympy_format": "-(dCl_tot_dt + target_Cl)/5.8401647 - 1.2372783", + "lambda_format": "PySRFunction(X=>-(dCl_tot_dt + target_Cl)/5.8401647 - 1.2372783)" + }, + { + "complexity": 10, + "loss": 0.019975036, + "equation": "-1.2351149 - ((dCl_tot_dt + target_Cl) / (target_Cd + Cd_rear))", + "score": 0.0488563493561329, + "sympy_format": "-1.2351149 - (dCl_tot_dt + target_Cl)/(Cd_rear + target_Cd)", + "lambda_format": "PySRFunction(X=>-1.2351149 - (dCl_tot_dt + target_Cl)/(Cd_rear + target_Cd))" + }, + { + "complexity": 11, + "loss": 0.018287793, + "equation": "-1.2386359 - ((target_Cl + (dCl_tot_dt + dCd_err_dt)) / 5.8050957)", + "score": 0.08824950581277256, + "sympy_format": "-(dCd_err_dt + dCl_tot_dt + target_Cl)/5.8050957 - 1.2386359", + "lambda_format": "PySRFunction(X=>-(dCd_err_dt + dCl_tot_dt + target_Cl)/5.8050957 - 1.2386359)" + }, + { + "complexity": 12, + "loss": 0.016175408, + "equation": "-1.2290057 - ((((dCd_err_dt + target_Cl) + dCl_tot_dt) / target_Cd) / Cd_rear)", + "score": 0.12274172391063341, + "sympy_format": "-1.2290057 - (dCd_err_dt + dCl_tot_dt + target_Cl)/(Cd_rear*target_Cd)", + "lambda_format": "PySRFunction(X=>-1.2290057 - (dCd_err_dt + dCl_tot_dt + target_Cl)/(Cd_rear*target_Cd))" + }, + { + "complexity": 13, + "loss": 0.01565276, + "equation": "-1.2277503 - ((((dCl_tot_dt + dCd_err_dt) + target_Cl) / 3.854052) / Cd_rear)", + "score": 0.03284480491569963, + "sympy_format": "-1.2277503 - (dCd_err_dt + dCl_tot_dt + target_Cl)/(3.854052*Cd_rear)", + "lambda_format": "PySRFunction(X=>-1.2277503 - (dCd_err_dt + dCl_tot_dt + target_Cl)/(3.854052*Cd_rear))" + }, + { + "complexity": 14, + "loss": 0.014618657, + "equation": "-1.1933857 - (((dCl_tot_dt + dCd_err_dt) + target_Cl) / ((target_Cd - target_Cl) * Cd_rear))", + "score": 0.0683486696273396, + "sympy_format": "-1.1933857 - (dCd_err_dt + dCl_tot_dt + target_Cl)/(Cd_rear*(target_Cd - target_Cl))", + "lambda_format": "PySRFunction(X=>-1.1933857 - (dCd_err_dt + dCl_tot_dt + target_Cl)/(Cd_rear*(target_Cd - target_Cl)))" + } + ] + }, + "top": { + "best_sympy": "Cd_tot*0.45037818 + Cl_err - 0.016348997*u_a", + "best_score": 0.8375927169907114, + "equations": [ + { + "complexity": 1, + "loss": 0.45252356, + "equation": "Cd_rear", + "score": 0.0, + "sympy_format": "Cd_rear", + "lambda_format": "PySRFunction(X=>Cd_rear)" + }, + { + "complexity": 2, + "loss": 0.1519909, + "equation": "1.8688794", + "score": 1.0910191774498823, + "sympy_format": "1.86887940000000", + "lambda_format": "PySRFunction(X=>1.86887940000000)" + }, + { + "complexity": 4, + "loss": 0.1296351, + "equation": "Cl_err + 1.8521743", + "score": 0.07954853502512871, + "sympy_format": "Cl_err + 1.8521743", + "lambda_format": "PySRFunction(X=>Cl_err + 1.8521743)" + }, + { + "complexity": 6, + "loss": 0.074802674, + "equation": "(8.007482 - dCl_tot_dt) / target_Cd", + "score": 0.2749349737891081, + "sympy_format": "(8.007482 - dCl_tot_dt)/target_Cd", + "lambda_format": "PySRFunction(X=>(8.007482 - dCl_tot_dt)/target_Cd)" + }, + { + "complexity": 7, + "loss": 0.06326852, + "equation": "1.8858196 - (u_a * 0.01622415)", + "score": 0.16746574193223032, + "sympy_format": "1.8858196 - 0.01622415*u_a", + "lambda_format": "PySRFunction(X=>1.8858196 - 0.01622415*u_a)" + }, + { + "complexity": 8, + "loss": 0.04728117, + "equation": "((Cl_tot + 7.9624605) - dCl_tot_dt) / target_Cd", + "score": 0.29127577208778055, + "sympy_format": "(Cl_tot - dCl_tot_dt + 7.9624605)/target_Cd", + "lambda_format": "PySRFunction(X=>(Cl_tot - dCl_tot_dt + 7.9624605)/target_Cd)" + }, + { + "complexity": 9, + "loss": 0.039528407, + "equation": "(Cl_err + 1.8692691) - (u_a * 0.016348997)", + "score": 0.17909254099542934, + "sympy_format": "Cl_err - 0.016348997*u_a + 1.8692691", + "lambda_format": "PySRFunction(X=>Cl_err - 0.016348997*u_a + 1.8692691)" + }, + { + "complexity": 10, + "loss": 0.037286226, + "equation": "(((target_Cd - dCl_tot_dt) * 1.8496238) + target_Cl) / target_Cd", + "score": 0.05839559567798887, + "sympy_format": "(target_Cl + (-dCl_tot_dt + target_Cd)*1.8496238)/target_Cd", + "lambda_format": "PySRFunction(X=>(target_Cl + (-dCl_tot_dt + target_Cd)*1.8496238)/target_Cd)" + }, + { + "complexity": 11, + "loss": 0.024684431, + "equation": "Cl_err + ((Cd_tot * 0.45037818) - (u_a * 0.016348997))", + "score": 0.41245126130146226, + "sympy_format": "Cd_tot*0.45037818 + Cl_err - 0.016348997*u_a", + "lambda_format": "PySRFunction(X=>Cd_tot*0.45037818 + Cl_err - 0.016348997*u_a)" + }, + { + "complexity": 13, + "loss": 0.018228356, + "equation": "(((dCl_err_dt + target_Cd) - dCl_tot_dt) * 0.4332428) + (du_a_dt / -94.089226)", + "score": 0.15159715870297544, + "sympy_format": "du_a_dt/(-94.089226) + (dCl_err_dt - dCl_tot_dt + target_Cd)*0.4332428", + "lambda_format": "PySRFunction(X=>du_a_dt/(-94.089226) + (dCl_err_dt - dCl_tot_dt + target_Cd)*0.4332428)" + }, + { + "complexity": 15, + "loss": 0.017791519, + "equation": "(((dCl_err_dt + target_Cd) - dCl_tot_dt) * 0.432934) + ((du_a_dt + dCl_tot_dt) / -91.89213)", + "score": 0.012128260261647603, + "sympy_format": "(dCl_tot_dt + du_a_dt)/(-91.89213) + (dCl_err_dt - dCl_tot_dt + target_Cd)*0.432934", + "lambda_format": "PySRFunction(X=>(dCl_tot_dt + du_a_dt)/(-91.89213) + (dCl_err_dt - dCl_tot_dt + target_Cd)*0.432934)" + } + ] + } + }, + "illusion_1L": { + "front": { + "best_sympy": "(du_a_dt + u_a + 26.506351)*0.012334249", + "best_score": 0.9288424393879982, + "equations": [ + { + "complexity": 1, + "loss": 0.29883155, + "equation": "dCd_err_dt", + "score": 0.0, + "sympy_format": "dCd_err_dt", + "lambda_format": "PySRFunction(X=>dCd_err_dt)" + }, + { + "complexity": 2, + "loss": 0.15649326, + "equation": "0.33446583", + "score": 0.6468670947471492, + "sympy_format": "0.334465830000000", + "lambda_format": "PySRFunction(X=>0.334465830000000)" + }, + { + "complexity": 4, + "loss": 0.14671756, + "equation": "1.1508491 / Cd_rear", + "score": 0.03225178195326072, + "sympy_format": "1.1508491/Cd_rear", + "lambda_format": "PySRFunction(X=>1.1508491/Cd_rear)" + }, + { + "complexity": 5, + "loss": 0.14011887, + "equation": "(Cd_tot - Cd_rear) / Cd_tot", + "score": 0.0460182442729821, + "sympy_format": "(-Cd_rear + Cd_tot)/Cd_tot", + "lambda_format": "PySRFunction(X=>(-Cd_rear + Cd_tot)/Cd_tot)" + }, + { + "complexity": 6, + "loss": 0.08638231, + "equation": "(Cl_tot + 1.8901986) / Cd_tot", + "score": 0.48370822430419386, + "sympy_format": "(Cl_tot + 1.8901986)/Cd_tot", + "lambda_format": "PySRFunction(X=>(Cl_tot + 1.8901986)/Cd_tot)" + }, + { + "complexity": 7, + "loss": 0.047962923, + "equation": "(u_a * 0.015167245) + 0.3176641", + "score": 0.5883546345521161, + "sympy_format": "u_a*0.015167245 + 0.3176641", + "lambda_format": "PySRFunction(X=>u_a*0.015167245 + 0.3176641)" + }, + { + "complexity": 8, + "loss": 0.04292227, + "equation": "((Cl_tot - dCl_tot_dt) / target_Cd) + 0.34500188", + "score": 0.11103746946441734, + "sympy_format": "0.34500188 + (Cl_tot - dCl_tot_dt)/target_Cd", + "lambda_format": "PySRFunction(X=>0.34500188 + (Cl_tot - dCl_tot_dt)/target_Cd)" + }, + { + "complexity": 9, + "loss": 0.011135677, + "equation": "(u_a + (du_a_dt + 26.506351)) * 0.012334249", + "score": 1.349236707317237, + "sympy_format": "(du_a_dt + u_a + 26.506351)*0.012334249", + "lambda_format": "PySRFunction(X=>(du_a_dt + u_a + 26.506351)*0.012334249)" + }, + { + "complexity": 11, + "loss": 0.010412063, + "equation": "((du_a_dt - dCl_tot_dt) + (u_a + 26.817722)) * 0.012179124", + "score": 0.03359453015897125, + "sympy_format": "(-dCl_tot_dt + du_a_dt + u_a + 26.817722)*0.012179124", + "lambda_format": "PySRFunction(X=>(-dCl_tot_dt + du_a_dt + u_a + 26.817722)*0.012179124)" + }, + { + "complexity": 12, + "loss": 0.009215188, + "equation": "(du_a_dt * 0.010480981) + ((u_a * 0.0138015505) + 0.32438707)", + "score": 0.12211204539179134, + "sympy_format": "du_a_dt*0.010480981 + u_a*0.0138015505 + 0.32438707", + "lambda_format": "PySRFunction(X=>du_a_dt*0.010480981 + u_a*0.0138015505 + 0.32438707)" + }, + { + "complexity": 13, + "loss": 0.00894252, + "equation": "(((du_a_dt + u_a) + 22.995256) - (du_a_dt / Cd_rear)) * 0.014093986", + "score": 0.030035563773362823, + "sympy_format": "(du_a_dt + u_a + 22.995256 - du_a_dt/Cd_rear)*0.014093986", + "lambda_format": "PySRFunction(X=>(du_a_dt + u_a + 22.995256 - du_a_dt/Cd_rear)*0.014093986)" + }, + { + "complexity": 14, + "loss": 0.008032124, + "equation": "((u_a + Cl_tot) + ((du_a_dt / 1.4181519) + 23.637499)) * 0.013763895", + "score": 0.10736842758236086, + "sympy_format": "(Cl_tot + du_a_dt/1.4181519 + u_a + 23.637499)*0.013763895", + "lambda_format": "PySRFunction(X=>(Cl_tot + du_a_dt/1.4181519 + u_a + 23.637499)*0.013763895)" + }, + { + "complexity": 15, + "loss": 0.0077406703, + "equation": "(du_a_dt + (((Cl_tot + 23.718166) - (du_a_dt / Cd_rear)) + u_a)) * 0.013728722", + "score": 0.0369607151581401, + "sympy_format": "(Cl_tot + du_a_dt + u_a + 23.718166 - du_a_dt/Cd_rear)*0.013728722", + "lambda_format": "PySRFunction(X=>(Cl_tot + du_a_dt + u_a + 23.718166 - du_a_dt/Cd_rear)*0.013728722)" + } + ] + }, + "top": { + "best_sympy": "dCl_tot_dt*(-0.13315448)*(Cl_tot + bias) + 0.80922115", + "best_score": 0.6947513140376443, + "equations": [ + { + "complexity": 1, + "loss": 0.11277538, + "equation": "bias", + "score": 0.0, + "sympy_format": "bias", + "lambda_format": "PySRFunction(X=>bias)" + }, + { + "complexity": 2, + "loss": 0.066365264, + "equation": "0.7845461", + "score": 0.530224265759199, + "sympy_format": "0.784546100000000", + "lambda_format": "PySRFunction(X=>0.784546100000000)" + }, + { + "complexity": 4, + "loss": 0.0651199, + "equation": "target_Cd + -4.860893", + "score": 0.00947180050206764, + "sympy_format": "target_Cd - 4.860893", + "lambda_format": "PySRFunction(X=>target_Cd - 4.860893)" + }, + { + "complexity": 5, + "loss": 0.05987383, + "equation": "Cd_rear / (Cd_tot + dCl_tot_dt)", + "score": 0.08399067123622497, + "sympy_format": "Cd_rear/(Cd_tot + dCl_tot_dt)", + "lambda_format": "PySRFunction(X=>Cd_rear/(Cd_tot + dCl_tot_dt))" + }, + { + "complexity": 6, + "loss": 0.03714929, + "equation": "0.77852744 - (dCl_tot_dt / target_Cd)", + "score": 0.47729485555849455, + "sympy_format": "0.77852744 - dCl_tot_dt/target_Cd", + "lambda_format": "PySRFunction(X=>0.77852744 - dCl_tot_dt/target_Cd)" + }, + { + "complexity": 7, + "loss": 0.03588954, + "equation": "(dCl_tot_dt * -0.14946803) + 0.77950186", + "score": 0.034498771135124374, + "sympy_format": "0.77950186 + dCl_tot_dt*(-0.14946803)", + "lambda_format": "PySRFunction(X=>0.77950186 + dCl_tot_dt*(-0.14946803))" + }, + { + "complexity": 8, + "loss": 0.028979579, + "equation": "(dCl_tot_dt / (Cl_tot - target_Cd)) + 0.78621364", + "score": 0.21385447860989099, + "sympy_format": "dCl_tot_dt/(Cl_tot - target_Cd) + 0.78621364", + "lambda_format": "PySRFunction(X=>dCl_tot_dt/(Cl_tot - target_Cd) + 0.78621364)" + }, + { + "complexity": 9, + "loss": 0.028761907, + "equation": "(dCl_tot_dt / (Cl_tot + -5.7742095)) + 0.7856848", + "score": 0.007539571500876667, + "sympy_format": "dCl_tot_dt/(Cl_tot - 5.7742095) + 0.7856848", + "lambda_format": "PySRFunction(X=>dCl_tot_dt/(Cl_tot - 5.7742095) + 0.7856848)" + }, + { + "complexity": 10, + "loss": 0.026970413, + "equation": "(dCl_tot_dt / ((Cl_tot + Cl_tot) - target_Cd)) + 0.80081576", + "score": 0.06431138768048111, + "sympy_format": "dCl_tot_dt/(Cl_tot + Cl_tot - target_Cd) + 0.80081576", + "lambda_format": "PySRFunction(X=>dCl_tot_dt/(Cl_tot + Cl_tot - target_Cd) + 0.80081576)" + }, + { + "complexity": 11, + "loss": 0.02025791, + "equation": "((dCl_tot_dt * -0.13315448) * (Cl_tot + bias)) + 0.80922115", + "score": 0.2861951157800486, + "sympy_format": "dCl_tot_dt*(-0.13315448)*(Cl_tot + bias) + 0.80922115", + "lambda_format": "PySRFunction(X=>dCl_tot_dt*(-0.13315448)*(Cl_tot + bias) + 0.80922115)" + }, + { + "complexity": 12, + "loss": 0.0187998, + "equation": "(((Cl_tot + 1.4032292) * dCl_tot_dt) * -0.11100898) + 0.80360377", + "score": 0.0746991030839848, + "sympy_format": "(Cl_tot + 1.4032292)*dCl_tot_dt*(-0.11100898) + 0.80360377", + "lambda_format": "PySRFunction(X=>(Cl_tot + 1.4032292)*dCl_tot_dt*(-0.11100898) + 0.80360377)" + }, + { + "complexity": 13, + "loss": 0.017390246, + "equation": "square(((dCl_tot_dt * (Cl_tot + 1.4040092)) * -0.065458074) + 0.88719195)", + "score": 0.0779367571543523, + "sympy_format": "(dCl_tot_dt*(Cl_tot + 1.4040092)*(-0.065458074) + 0.88719195)**2", + "lambda_format": "PySRFunction(X=>(dCl_tot_dt*(Cl_tot + 1.4040092)*(-0.065458074) + 0.88719195)**2)" + }, + { + "complexity": 14, + "loss": 0.015813593, + "equation": "((dCl_tot_dt * -0.11452771) * ((Cl_err + Cl_tot) + 1.4403551)) + 0.78953165", + "score": 0.09503958769856034, + "sympy_format": "dCl_tot_dt*(-0.11452771)*(Cl_err + Cl_tot + 1.4403551) + 0.78953165", + "lambda_format": "PySRFunction(X=>dCl_tot_dt*(-0.11452771)*(Cl_err + Cl_tot + 1.4403551) + 0.78953165)" + }, + { + "complexity": 15, + "loss": 0.013981272, + "equation": "(((square(Cl_err + 1.2575057) + Cl_tot) * -0.1052603) * dCl_tot_dt) + 0.76740843", + "score": 0.12315116683748104, + "sympy_format": "(Cl_tot + (Cl_err + 1.2575057)**2)*(-0.1052603)*dCl_tot_dt + 0.76740843", + "lambda_format": "PySRFunction(X=>(Cl_tot + (Cl_err + 1.2575057)**2)*(-0.1052603)*dCl_tot_dt + 0.76740843)" + } + ] + } + } +} \ No newline at end of file diff --git a/src/SR_analysis/validate/results/archive/pysr_illusion_front.json b/src/SR_analysis/validate/results/archive/pysr_illusion_front.json new file mode 100644 index 0000000..894d663 --- /dev/null +++ b/src/SR_analysis/validate/results/archive/pysr_illusion_front.json @@ -0,0 +1,41 @@ +{ + "scene": "illusion_1L", + "channel": "front", + "feature_names": [ + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "aF_lag1", + "daF", + "daT" + ], + "equations": [ + { + "complexity": 1, + "loss": 0.010047342, + "equation": "daF", + "score": 0.0, + "sympy_format": "daF", + "lambda_format": "PySRFunction(X=>daF)" + }, + { + "complexity": 2, + "loss": 9.389894e-06, + "equation": "0.0015919553", + "score": 6.975429396343354, + "sympy_format": "0.00159195530000000", + "lambda_format": "PySRFunction(X=>0.00159195530000000)" + }, + { + "complexity": 4, + "loss": 1.2318608e-20, + "equation": "aF_lag1 * 0.01", + "score": 17.133649717376684, + "sympy_format": "aF_lag1*0.01", + "lambda_format": "PySRFunction(X=>aF_lag1*0.01)" + } + ], + "best_sympy": "aF_lag1*0.01", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/validate/results/archive/pysr_illusion_joint_deep.json b/src/SR_analysis/validate/results/archive/pysr_illusion_joint_deep.json new file mode 100644 index 0000000..2fbadbc --- /dev/null +++ b/src/SR_analysis/validate/results/archive/pysr_illusion_joint_deep.json @@ -0,0 +1,198 @@ +{ + "front": { + "best_sympy": "Cd_tot - (Cd_err + 5.4276643) - (-0.009783858)*(du_a_dt + u_a)", + "best_score": 0.9072325432033944, + "equations": [ + { + "complexity": 1, + "loss": 0.9253167, + "equation": "Cd_err", + "score": 0.0, + "sympy_format": "Cd_err", + "lambda_format": "PySRFunction(X=>Cd_err)" + }, + { + "complexity": 2, + "loss": 0.73551357, + "equation": "-0.45253906", + "score": 0.22956706738741847, + "sympy_format": "-0.452539060000000", + "lambda_format": "PySRFunction(X=>-0.452539060000000)" + }, + { + "complexity": 4, + "loss": 0.25328484, + "equation": "Cd_tot + -5.182412", + "score": 0.5330271423344977, + "sympy_format": "Cd_tot - 5.182412", + "lambda_format": "PySRFunction(X=>Cd_tot - 5.182412)" + }, + { + "complexity": 6, + "loss": 0.13145858, + "equation": "(Cd_tot - Cd_err) - 5.4218307", + "score": 0.3279114421119962, + "sympy_format": "-Cd_err + Cd_tot - 1*5.4218307", + "lambda_format": "PySRFunction(X=>-Cd_err + Cd_tot - 1*5.4218307)" + }, + { + "complexity": 9, + "loss": 0.12025861, + "equation": "((Cd_tot - Cd_err) * 1.1559658) + -6.196875", + "score": 0.029682437919346997, + "sympy_format": "(-Cd_err + Cd_tot)*1.1559658 - 6.196875", + "lambda_format": "PySRFunction(X=>(-Cd_err + Cd_tot)*1.1559658 - 6.196875)" + }, + { + "complexity": 10, + "loss": 0.09853291, + "equation": "Cd_tot - (Cd_err + ((dCl_tot_dt / Cd_tot) + 5.4268394))", + "score": 0.19925390320396055, + "sympy_format": "Cd_tot - (Cd_err + 5.4268394 + dCl_tot_dt/Cd_tot)", + "lambda_format": "PySRFunction(X=>Cd_tot - (Cd_err + 5.4268394 + dCl_tot_dt/Cd_tot))" + }, + { + "complexity": 11, + "loss": 0.08889836, + "equation": "(Cd_tot - (du_a_dt * -0.010945146)) - (Cd_err + 5.422836)", + "score": 0.10289690939463386, + "sympy_format": "Cd_tot - (-0.010945146)*du_a_dt - (Cd_err + 5.422836)", + "lambda_format": "PySRFunction(X=>Cd_tot - (-0.010945146)*du_a_dt - (Cd_err + 5.422836))" + }, + { + "complexity": 13, + "loss": 0.068231724, + "equation": "(Cd_tot - ((du_a_dt + u_a) * -0.009783858)) - (Cd_err + 5.4276643)", + "score": 0.13229203833494793, + "sympy_format": "Cd_tot - (Cd_err + 5.4276643) - (-0.009783858)*(du_a_dt + u_a)", + "lambda_format": "PySRFunction(X=>Cd_tot - (Cd_err + 5.4276643) - (-0.009783858)*(du_a_dt + u_a))" + }, + { + "complexity": 15, + "loss": 0.06452486, + "equation": "(Cd_tot - (du_a_dt * -0.009783517)) - (((dCl_tot_dt / Cd_tot) + Cd_err) + 5.4276643)", + "score": 0.02792952104255307, + "sympy_format": "Cd_tot - (-0.009783517)*du_a_dt - (Cd_err + 5.4276643 + dCl_tot_dt/Cd_tot)", + "lambda_format": "PySRFunction(X=>Cd_tot - (-0.009783517)*du_a_dt - (Cd_err + 5.4276643 + dCl_tot_dt/Cd_tot))" + } + ] + }, + "top": { + "best_sympy": "(Cd_err - (Cd_rear - Cl_err))*0.53502613 + 2.7819264", + "best_score": 0.8278631696385599, + "equations": [ + { + "complexity": 1, + "loss": 0.5098592, + "equation": "bias", + "score": 0.0, + "sympy_format": "bias", + "lambda_format": "PySRFunction(X=>bias)" + }, + { + "complexity": 2, + "loss": 0.40311, + "equation": "1.3267554", + "score": 0.2349251316149049, + "sympy_format": "1.32675540000000", + "lambda_format": "PySRFunction(X=>1.32675540000000)" + }, + { + "complexity": 4, + "loss": 0.25452837, + "equation": "6.2989798 / Cd_tot", + "score": 0.22989858715007516, + "sympy_format": "6.2989798/Cd_tot", + "lambda_format": "PySRFunction(X=>6.2989798/Cd_tot)" + }, + { + "complexity": 5, + "loss": 0.20017381, + "equation": "square(5.275764 / Cd_tot)", + "score": 0.2402262641125521, + "sympy_format": "27.833685783696/Cd_tot**2", + "lambda_format": "PySRFunction(X=>27.833685783696/Cd_tot**2)" + }, + { + "complexity": 6, + "loss": 0.13331908, + "equation": "Cd_err + (6.296035 - Cd_tot)", + "score": 0.4064406864170655, + "sympy_format": "Cd_err - Cd_tot + 6.296035", + "lambda_format": "PySRFunction(X=>Cd_err - Cd_tot + 6.296035)" + }, + { + "complexity": 7, + "loss": 0.13072978, + "equation": "2.5826628 - (Cd_rear * 0.51413125)", + "score": 0.019612908011106035, + "sympy_format": "2.5826628 - 0.51413125*Cd_rear", + "lambda_format": "PySRFunction(X=>2.5826628 - 0.51413125*Cd_rear)" + }, + { + "complexity": 8, + "loss": 0.1229477, + "equation": "square(square(-5.049546 / (Cd_tot - Cd_err)))", + "score": 0.061373383018705346, + "sympy_format": "650.14365945995/(-Cd_err + Cd_tot)**4", + "lambda_format": "PySRFunction(X=>650.14365945995/(-Cd_err + Cd_tot)**4)" + }, + { + "complexity": 9, + "loss": 0.08865507, + "equation": "((Cd_err - Cd_rear) * 0.5703471) + 2.8565276", + "score": 0.32700583960963264, + "sympy_format": "(Cd_err - Cd_rear)*0.5703471 + 2.8565276", + "lambda_format": "PySRFunction(X=>(Cd_err - Cd_rear)*0.5703471 + 2.8565276)" + }, + { + "complexity": 10, + "loss": 0.08068954, + "equation": "(Cd_err - ((dCl_tot_dt / Cd_tot) + Cd_tot)) + 6.2911167", + "score": 0.09414427107238907, + "sympy_format": "Cd_err - (Cd_tot + dCl_tot_dt/Cd_tot) + 6.2911167", + "lambda_format": "PySRFunction(X=>Cd_err - (Cd_tot + dCl_tot_dt/Cd_tot) + 6.2911167)" + }, + { + "complexity": 11, + "loss": 0.06939008, + "equation": "((Cd_err - (Cd_rear - Cl_err)) * 0.53502613) + 2.7819264", + "score": 0.1508650331991575, + "sympy_format": "(Cd_err - (Cd_rear - Cl_err))*0.53502613 + 2.7819264", + "lambda_format": "PySRFunction(X=>(Cd_err - (Cd_rear - Cl_err))*0.53502613 + 2.7819264)" + }, + { + "complexity": 12, + "loss": 0.0650801, + "equation": "square((((Cl_err - Cd_rear) + Cd_err) * 0.24273622) + 1.7814205)", + "score": 0.06412509889463819, + "sympy_format": "((Cd_err - Cd_rear + Cl_err)*0.24273622 + 1.7814205)**2", + "lambda_format": "PySRFunction(X=>((Cd_err - Cd_rear + Cl_err)*0.24273622 + 1.7814205)**2)" + }, + { + "complexity": 13, + "loss": 0.063523956, + "equation": "square(square((dCl_tot_dt / -19.003147) - (-5.0304327 / (Cd_tot - Cd_err))))", + "score": 0.024201724323467403, + "sympy_format": "(dCl_tot_dt/(-19.003147) - (-1)*5.0304327/(-Cd_err + Cd_tot))**4", + "lambda_format": "PySRFunction(X=>(dCl_tot_dt/(-19.003147) - (-1)*5.0304327/(-Cd_err + Cd_tot))**4)" + }, + { + "complexity": 14, + "loss": 0.057359777, + "equation": "(Cd_err + (6.5962768 - Cd_tot)) / ((dCl_tot_dt * 0.21387522) + 1.2665501)", + "score": 0.10207378603939526, + "sympy_format": "(Cd_err - Cd_tot + 6.5962768)/(dCl_tot_dt*0.21387522 + 1.2665501)", + "lambda_format": "PySRFunction(X=>(Cd_err - Cd_tot + 6.5962768)/(dCl_tot_dt*0.21387522 + 1.2665501))" + }, + { + "complexity": 15, + "loss": 0.05449361, + "equation": "square(square(((dCl_tot_dt - Cl_tot) / -21.99823) - (-5.0213413 / (Cd_tot - Cd_err))))", + "score": 0.0512598614684422, + "sympy_format": "((-Cl_tot + dCl_tot_dt)/(-21.99823) - (-1)*5.0213413/(-Cd_err + Cd_tot))**4", + "lambda_format": "PySRFunction(X=>((-Cl_tot + dCl_tot_dt)/(-21.99823) - (-1)*5.0213413/(-Cd_err + Cd_tot))**4)" + } + ] + } +} \ No newline at end of file diff --git a/src/SR_analysis/validate/results/archive/pysr_illusion_joint_marker_deep.json b/src/SR_analysis/validate/results/archive/pysr_illusion_joint_marker_deep.json new file mode 100644 index 0000000..a08b3ef --- /dev/null +++ b/src/SR_analysis/validate/results/archive/pysr_illusion_joint_marker_deep.json @@ -0,0 +1,214 @@ +{ + "front": { + "best_sympy": "target_diameter*((du_a_dt + u_a)*0.010333712 + 6.324169) - 5.9960093", + "best_score": 0.9415886883621589, + "equations": [ + { + "complexity": 1, + "loss": 0.9253167, + "equation": "Cd_err", + "score": 0.0, + "sympy_format": "Cd_err", + "lambda_format": "PySRFunction(X=>Cd_err)" + }, + { + "complexity": 2, + "loss": 0.73551357, + "equation": "-0.45248327", + "score": 0.22956706738741847, + "sympy_format": "-0.452483270000000", + "lambda_format": "PySRFunction(X=>-0.452483270000000)" + }, + { + "complexity": 4, + "loss": 0.25328484, + "equation": "Cd_tot + -5.182412", + "score": 0.5330271423344977, + "sympy_format": "Cd_tot - 5.182412", + "lambda_format": "PySRFunction(X=>Cd_tot - 5.182412)" + }, + { + "complexity": 5, + "loss": 0.1827862, + "equation": "Cd_tot - (Cd_tot / target_diameter)", + "score": 0.3261975414072848, + "sympy_format": "Cd_tot - Cd_tot/target_diameter", + "lambda_format": "PySRFunction(X=>Cd_tot - Cd_tot/target_diameter)" + }, + { + "complexity": 6, + "loss": 0.13145858, + "equation": "Cd_tot + (-5.4218307 - Cd_err)", + "score": 0.3296253428167075, + "sympy_format": "-Cd_err + Cd_tot - 5.4218307", + "lambda_format": "PySRFunction(X=>-Cd_err + Cd_tot - 5.4218307)" + }, + { + "complexity": 7, + "loss": 0.11614521, + "equation": "(target_diameter * 6.29599) + -5.9614997", + "score": 0.12385060242241161, + "sympy_format": "target_diameter*6.29599 - 5.9614997", + "lambda_format": "PySRFunction(X=>target_diameter*6.29599 - 5.9614997)" + }, + { + "complexity": 8, + "loss": 0.1161452, + "equation": "-3.2632153 - (square(target_diameter) * -3.597705)", + "score": 8.609912043160935e-08, + "sympy_format": "-(-3.597705)*target_diameter**2 - 3.2632153", + "lambda_format": "PySRFunction(X=>-(-3.597705)*target_diameter**2 - 3.2632153)" + }, + { + "complexity": 9, + "loss": 0.08767577, + "equation": "(target_diameter - 0.94178987) * (Cl_tot + 6.3685937)", + "score": 0.281195554067957, + "sympy_format": "(Cl_tot + 6.3685937)*(target_diameter - 1*0.94178987)", + "lambda_format": "PySRFunction(X=>(Cl_tot + 6.3685937)*(target_diameter - 1*0.94178987))" + }, + { + "complexity": 11, + "loss": 0.08353403, + "equation": "((Cl_tot + 6.34856) - Cl_err) * (target_diameter - 0.94314027)", + "score": 0.02419574233984359, + "sympy_format": "(target_diameter - 1*0.94314027)*(-Cl_err + Cl_tot + 6.34856)", + "lambda_format": "PySRFunction(X=>(target_diameter - 1*0.94314027)*(-Cl_err + Cl_tot + 6.34856))" + }, + { + "complexity": 12, + "loss": 0.07040261, + "equation": "((6.3476257 - (du_a_dt * -0.012814552)) * target_diameter) + -6.0067677", + "score": 0.17102375740270453, + "sympy_format": "target_diameter*(6.3476257 - (-0.012814552)*du_a_dt) - 6.0067677", + "lambda_format": "PySRFunction(X=>target_diameter*(6.3476257 - (-0.012814552)*du_a_dt) - 6.0067677)" + }, + { + "complexity": 14, + "loss": 0.042962316, + "equation": "(target_diameter * (((du_a_dt + u_a) * 0.010333712) + 6.324169)) + -5.9960093", + "score": 0.2469534884892025, + "sympy_format": "target_diameter*((du_a_dt + u_a)*0.010333712 + 6.324169) - 5.9960093", + "lambda_format": "PySRFunction(X=>target_diameter*((du_a_dt + u_a)*0.010333712 + 6.324169) - 5.9960093)" + } + ] + }, + "top": { + "best_sympy": "(Cd_tot - dCl_tot_dt)*0.14305054/target_diameter**4", + "best_score": 0.8604861666442539, + "equations": [ + { + "complexity": 1, + "loss": 0.5098592, + "equation": "bias", + "score": 0.0, + "sympy_format": "bias", + "lambda_format": "PySRFunction(X=>bias)" + }, + { + "complexity": 2, + "loss": 0.40311, + "equation": "1.3266717", + "score": 0.2349251316149049, + "sympy_format": "1.32667170000000", + "lambda_format": "PySRFunction(X=>1.32667170000000)" + }, + { + "complexity": 3, + "loss": 0.2757882, + "equation": "bias / target_diameter", + "score": 0.37957629773805596, + "sympy_format": "bias/target_diameter", + "lambda_format": "PySRFunction(X=>bias/target_diameter)" + }, + { + "complexity": 4, + "loss": 0.13653293, + "equation": "square(bias / target_diameter)", + "score": 0.7030673488661097, + "sympy_format": "bias**2/target_diameter**2", + "lambda_format": "PySRFunction(X=>bias**2/target_diameter**2)" + }, + { + "complexity": 5, + "loss": 0.12044785, + "equation": "(Cd_tot - Cd_rear) - target_diameter", + "score": 0.12534895177986893, + "sympy_format": "-Cd_rear + Cd_tot - target_diameter", + "lambda_format": "PySRFunction(X=>-Cd_rear + Cd_tot - target_diameter)" + }, + { + "complexity": 6, + "loss": 0.111193664, + "equation": "square(square(target_diameter + -1.9235482))", + "score": 0.07994347739550943, + "sympy_format": "(target_diameter - 1.9235482)**4", + "lambda_format": "PySRFunction(X=>(target_diameter - 1.9235482)**4)" + }, + { + "complexity": 7, + "loss": 0.10917808, + "equation": "(3.252928 / target_diameter) + -2.4683578", + "score": 0.01829309121529708, + "sympy_format": "-2.4683578 + 3.252928/target_diameter", + "lambda_format": "PySRFunction(X=>-2.4683578 + 3.252928/target_diameter)" + }, + { + "complexity": 8, + "loss": 0.099606246, + "equation": "((Cl_err + Cd_tot) * 0.8038119) - Cd_rear", + "score": 0.09175543709290375, + "sympy_format": "-Cd_rear + (Cd_tot + Cl_err)*0.8038119", + "lambda_format": "PySRFunction(X=>-Cd_rear + (Cd_tot + Cl_err)*0.8038119)" + }, + { + "complexity": 9, + "loss": 0.07836932, + "equation": "bias / ((dCl_tot_dt * 0.10766987) + square(target_diameter))", + "score": 0.23979234923014134, + "sympy_format": "bias/(dCl_tot_dt*0.10766987 + target_diameter**2)", + "lambda_format": "PySRFunction(X=>bias/(dCl_tot_dt*0.10766987 + target_diameter**2))" + }, + { + "complexity": 10, + "loss": 0.056239426, + "equation": "((Cd_tot - dCl_tot_dt) * 0.14305054) / square(square(target_diameter))", + "score": 0.3318144830461071, + "sympy_format": "(Cd_tot - dCl_tot_dt)*0.14305054/target_diameter**4", + "lambda_format": "PySRFunction(X=>(Cd_tot - dCl_tot_dt)*0.14305054/target_diameter**4)" + }, + { + "complexity": 11, + "loss": 0.052204557, + "equation": "square(square((dCl_tot_dt * -0.052007385) - (target_diameter + -1.9191152)))", + "score": 0.07444825126258998, + "sympy_format": "(dCl_tot_dt*(-0.052007385) - (target_diameter - 1.9191152))**4", + "lambda_format": "PySRFunction(X=>(dCl_tot_dt*(-0.052007385) - (target_diameter - 1.9191152))**4)" + }, + { + "complexity": 12, + "loss": 0.04686619, + "equation": "((dCl_err_dt + (Cd_tot - dCl_tot_dt)) * 0.14262073) / square(square(target_diameter))", + "score": 0.10787326996912461, + "sympy_format": "(Cd_tot + dCl_err_dt - dCl_tot_dt)*0.14262073/target_diameter**4", + "lambda_format": "PySRFunction(X=>(Cd_tot + dCl_err_dt - dCl_tot_dt)*0.14262073/target_diameter**4)" + }, + { + "complexity": 14, + "loss": 0.04307425, + "equation": "(((dCl_err_dt + (Cd_tot + target_diameter)) - dCl_tot_dt) * 0.12152494) / square(square(target_diameter))", + "score": 0.04218557459444655, + "sympy_format": "(Cd_tot + dCl_err_dt - dCl_tot_dt + target_diameter)*0.12152494/target_diameter**4", + "lambda_format": "PySRFunction(X=>(Cd_tot + dCl_err_dt - dCl_tot_dt + target_diameter)*0.12152494/target_diameter**4)" + }, + { + "complexity": 15, + "loss": 0.038837627, + "equation": "(((Cl_tot * 0.46307632) + (Cd_tot - dCl_tot_dt)) / square(square(target_diameter))) * 0.1429364", + "score": 0.10353582602160404, + "sympy_format": "(Cd_tot + Cl_tot*0.46307632 - dCl_tot_dt)*0.1429364/target_diameter**4", + "lambda_format": "PySRFunction(X=>(Cd_tot + Cl_tot*0.46307632 - dCl_tot_dt)*0.1429364/target_diameter**4)" + } + ] + } +} \ No newline at end of file diff --git a/src/SR_analysis/validate/results/archive/pysr_illusion_top.json b/src/SR_analysis/validate/results/archive/pysr_illusion_top.json new file mode 100644 index 0000000..58a7b20 --- /dev/null +++ b/src/SR_analysis/validate/results/archive/pysr_illusion_top.json @@ -0,0 +1,43 @@ +{ + "scene": "illusion_1L", + "channel": "top", + "feature_names": [ + "bias", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "aB_lag1", + "aT_lag1", + "daF", + "daT" + ], + "equations": [ + { + "complexity": 1, + "loss": 0.010208026, + "equation": "daF", + "score": 0.0, + "sympy_format": "daF", + "lambda_format": "PySRFunction(X=>daF)" + }, + { + "complexity": 2, + "loss": 4.927774e-06, + "equation": "0.005665578", + "score": 7.636042187813283, + "sympy_format": "0.00566557800000000", + "lambda_format": "PySRFunction(X=>0.00566557800000000)" + }, + { + "complexity": 4, + "loss": 8.0356907e-20, + "equation": "aT_lag1 * 0.01", + "score": 15.873592854896415, + "sympy_format": "aT_lag1*0.01", + "lambda_format": "PySRFunction(X=>aT_lag1*0.01)" + } + ], + "best_sympy": "aT_lag1*0.01", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/validate/results/archive/pysr_karman_front.json b/src/SR_analysis/validate/results/archive/pysr_karman_front.json new file mode 100644 index 0000000..67a42ae --- /dev/null +++ b/src/SR_analysis/validate/results/archive/pysr_karman_front.json @@ -0,0 +1,35 @@ +{ + "scene": "karman_re100", + "channel": "front", + "feature_names": [ + "aF_lag1" + ], + "equations": [ + { + "complexity": 1, + "loss": 1.9439806, + "equation": "aF_lag1", + "score": 0.0, + "sympy_format": "aF_lag1", + "lambda_format": "PySRFunction(X=>aF_lag1)" + }, + { + "complexity": 2, + "loss": 0.00018302063, + "equation": "-0.00391465", + "score": 9.270649405800802, + "sympy_format": "-0.00391465000000000", + "lambda_format": "PySRFunction(X=>-0.00391465000000000)" + }, + { + "complexity": 4, + "loss": 3.3501009e-19, + "equation": "aF_lag1 * 0.01", + "score": 16.967107311432134, + "sympy_format": "aF_lag1*0.01", + "lambda_format": "PySRFunction(X=>aF_lag1*0.01)" + } + ], + "best_sympy": "aF_lag1*0.01", + "best_score": 1.0 +} \ No newline at end of file diff --git a/src/SR_analysis/validate/results/archive/pysr_karman_joint.json b/src/SR_analysis/validate/results/archive/pysr_karman_joint.json new file mode 100644 index 0000000..5e28029 --- /dev/null +++ b/src/SR_analysis/validate/results/archive/pysr_karman_joint.json @@ -0,0 +1,183 @@ +{ + "joint": true, + "scenes": [ + "karman_re50", + "karman_re100", + "karman_re200", + "karman_re400" + ], + "use_mu": false, + "feature_keys": [ + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear" + ], + "n_samples": 742, + "front": { + "best_sympy": "-0.200723950000000", + "best_score": -2.9948243884803105e-10, + "equations": [ + { + "complexity": 1, + "loss": 7.672221, + "equation": "Cd_rear", + "score": 0.0, + "sympy_format": "Cd_rear", + "lambda_format": "PySRFunction(X=>Cd_rear)" + }, + { + "complexity": 2, + "loss": 3.35684, + "equation": "-0.20072395", + "score": 0.8266060879547913, + "sympy_format": "-0.200723950000000", + "lambda_format": "PySRFunction(X=>-0.200723950000000)" + }, + { + "complexity": 4, + "loss": 3.0161395, + "equation": "du_a_dt * -0.037954144", + "score": 0.053511176433859185, + "sympy_format": "du_a_dt*(-0.037954144)", + "lambda_format": "PySRFunction(X=>du_a_dt*(-0.037954144))" + }, + { + "complexity": 6, + "loss": 2.737382, + "equation": "(du_a_dt * -0.016353678) * Cd_tot", + "score": 0.04848785660048393, + "sympy_format": "du_a_dt*(-0.016353678)*Cd_tot", + "lambda_format": "PySRFunction(X=>du_a_dt*(-0.016353678)*Cd_tot)" + }, + { + "complexity": 8, + "loss": 2.673239, + "equation": "(du_a_dt * (Cd_rear - Cd_tot)) * 0.016410492", + "score": 0.011855571693031036, + "sympy_format": "du_a_dt*(Cd_rear - Cd_tot)*0.016410492", + "lambda_format": "PySRFunction(X=>du_a_dt*(Cd_rear - Cd_tot)*0.016410492)" + }, + { + "complexity": 9, + "loss": 2.6436589, + "equation": "((du_a_dt * Cd_tot) * -0.017141027) - 0.30863574", + "score": 0.01112694121478059, + "sympy_format": "du_a_dt*Cd_tot*(-0.017141027) - 1*0.30863574", + "lambda_format": "PySRFunction(X=>du_a_dt*Cd_tot*(-0.017141027) - 1*0.30863574)" + }, + { + "complexity": 10, + "loss": 2.5950696, + "equation": "((Cd_tot - (Cd_rear - Cd_tot)) * du_a_dt) * -0.009496993", + "score": 0.018550567522468267, + "sympy_format": "(Cd_tot - (Cd_rear - Cd_tot))*du_a_dt*(-0.009496993)", + "lambda_format": "PySRFunction(X=>(Cd_tot - (Cd_rear - Cd_tot))*du_a_dt*(-0.009496993))" + }, + { + "complexity": 11, + "loss": 2.5218232, + "equation": "((du_a_dt * Cd_tot) * -0.016758118) - (Cl_tot * 0.18500322)", + "score": 0.028631205074821347, + "sympy_format": "du_a_dt*Cd_tot*(-0.016758118) - 0.18500322*Cl_tot", + "lambda_format": "PySRFunction(X=>du_a_dt*Cd_tot*(-0.016758118) - 0.18500322*Cl_tot)" + }, + { + "complexity": 13, + "loss": 2.339323, + "equation": "(((du_a_dt * -0.07838304) * (Cd_tot - Cd_rear)) - Cl_tot) * 0.23325484", + "score": 0.037560280361165564, + "sympy_format": "(-Cl_tot + du_a_dt*(-0.07838304)*(-Cd_rear + Cd_tot))*0.23325484", + "lambda_format": "PySRFunction(X=>(-Cl_tot + du_a_dt*(-0.07838304)*(-Cd_rear + Cd_tot))*0.23325484)" + } + ] + }, + "top": { + "best_sympy": "3.41383670000000", + "best_score": -1.9487522706640448e-11, + "equations": [ + { + "complexity": 1, + "loss": 14.187922, + "equation": "bias", + "score": 0.0, + "sympy_format": "bias", + "lambda_format": "PySRFunction(X=>bias)" + }, + { + "complexity": 2, + "loss": 8.361251, + "equation": "3.4138367", + "score": 0.5287829822428393, + "sympy_format": "3.41383670000000", + "lambda_format": "PySRFunction(X=>3.41383670000000)" + }, + { + "complexity": 7, + "loss": 7.777738, + "equation": "(du_a_dt * 0.046973832) + 3.4085016", + "score": 0.014468501327093677, + "sympy_format": "du_a_dt*0.046973832 + 3.4085016", + "lambda_format": "PySRFunction(X=>du_a_dt*0.046973832 + 3.4085016)" + }, + { + "complexity": 8, + "loss": 7.7693896, + "equation": "square((du_a_dt * -0.012934048) + -1.8339856)", + "score": 0.0010739476792349307, + "sympy_format": "(du_a_dt*(-0.012934048) - 1.8339856)**2", + "lambda_format": "PySRFunction(X=>(du_a_dt*(-0.012934048) - 1.8339856)**2)" + }, + { + "complexity": 9, + "loss": 7.0261855, + "equation": "((du_a_dt - u_a) * 0.05669279) + 3.4059103", + "score": 0.10054764730433441, + "sympy_format": "(du_a_dt - u_a)*0.05669279 + 3.4059103", + "lambda_format": "PySRFunction(X=>(du_a_dt - u_a)*0.05669279 + 3.4059103)" + }, + { + "complexity": 11, + "loss": 6.9558244, + "equation": "((u_a - (du_a_dt + Cd_rear)) * -0.05827892) + 3.431069", + "score": 0.0050323018253579065, + "sympy_format": "3.431069 + (u_a - (Cd_rear + du_a_dt))*(-0.05827892)", + "lambda_format": "PySRFunction(X=>3.431069 + (u_a - (Cd_rear + du_a_dt))*(-0.05827892))" + }, + { + "complexity": 12, + "loss": 6.8052278, + "equation": "(((square(Cl_tot) + u_a) - du_a_dt) * -0.055585466) + 3.756772", + "score": 0.021888240851175078, + "sympy_format": "3.756772 + (Cl_tot**2 - du_a_dt + u_a)*(-0.055585466)", + "lambda_format": "PySRFunction(X=>3.756772 + (Cl_tot**2 - du_a_dt + u_a)*(-0.055585466))" + }, + { + "complexity": 13, + "loss": 6.7258425, + "equation": "((du_a_dt - (u_a + (Cl_tot * Cd_rear))) * 0.06536462) + 3.4111843", + "score": 0.011733914477310068, + "sympy_format": "(du_a_dt - (Cd_rear*Cl_tot + u_a))*0.06536462 + 3.4111843", + "lambda_format": "PySRFunction(X=>(du_a_dt - (Cd_rear*Cl_tot + u_a))*0.06536462 + 3.4111843)" + }, + { + "complexity": 14, + "loss": 6.7159348, + "equation": "((square(Cl_tot) + ((u_a + Cl_tot) - du_a_dt)) * -0.055585466) + 3.756772", + "score": 0.001474165443025774, + "sympy_format": "3.756772 + (Cl_tot**2 + Cl_tot - du_a_dt + u_a)*(-0.055585466)", + "lambda_format": "PySRFunction(X=>3.756772 + (Cl_tot**2 + Cl_tot - du_a_dt + u_a)*(-0.055585466))" + }, + { + "complexity": 15, + "loss": 6.636368, + "equation": "(((du_a_dt - (u_a + (Cl_tot * Cd_rear))) - Cl_tot) * 0.06536462) + 3.4111843", + "score": 0.011918205124500161, + "sympy_format": "(-Cl_tot + du_a_dt - (Cd_rear*Cl_tot + u_a))*0.06536462 + 3.4111843", + "lambda_format": "PySRFunction(X=>(-Cl_tot + du_a_dt - (Cd_rear*Cl_tot + u_a))*0.06536462 + 3.4111843)" + } + ] + } +} \ No newline at end of file diff --git a/src/SR_analysis/validate/results/archive/pysr_karman_joint_front.json b/src/SR_analysis/validate/results/archive/pysr_karman_joint_front.json new file mode 100644 index 0000000..c3724e3 --- /dev/null +++ b/src/SR_analysis/validate/results/archive/pysr_karman_joint_front.json @@ -0,0 +1,30 @@ +{ + "scene": "karman_joint", + "scene_id": "karman", + "channel": "front", + "output": "alpha", + "feature_names": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "best_sympy": "(aF_lag1 + daF)*0.8435828", + "best_score": 0.9378936963643454 +} \ No newline at end of file diff --git a/src/SR_analysis/validate/results/archive/pysr_karman_joint_mu.json b/src/SR_analysis/validate/results/archive/pysr_karman_joint_mu.json new file mode 100644 index 0000000..1a94a9b --- /dev/null +++ b/src/SR_analysis/validate/results/archive/pysr_karman_joint_mu.json @@ -0,0 +1,213 @@ +{ + "joint": true, + "scenes": [ + "karman_re50", + "karman_re100", + "karman_re200", + "karman_re400" + ], + "use_mu": true, + "feature_keys": [ + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "n_samples": 742, + "front": { + "best_sympy": "-du_a_dt*mu_Cd_tot + mu_Cl_tot*(-19.611128)", + "best_score": 0.4322745894175589, + "equations": [ + { + "complexity": 1, + "loss": 3.279333, + "equation": "mu_Cl_diff", + "score": 0.0, + "sympy_format": "mu_Cl_diff", + "lambda_format": "PySRFunction(X=>mu_Cl_diff)" + }, + { + "complexity": 3, + "loss": 2.955941, + "equation": "Cl_tot * mu_Cl_diff", + "score": 0.051911502288096975, + "sympy_format": "Cl_tot*mu_Cl_diff", + "lambda_format": "PySRFunction(X=>Cl_tot*mu_Cl_diff)" + }, + { + "complexity": 4, + "loss": 2.5313113, + "equation": "mu_Cl_tot * -18.692875", + "score": 0.1550795746187338, + "sympy_format": "mu_Cl_tot*(-18.692875)", + "lambda_format": "PySRFunction(X=>mu_Cl_tot*(-18.692875))" + }, + { + "complexity": 6, + "loss": 2.4194217, + "equation": "(mu_Cl_tot * -19.746626) - mu_u_a", + "score": 0.022604462097066104, + "sympy_format": "mu_Cl_tot*(-19.746626) - mu_u_a", + "lambda_format": "PySRFunction(X=>mu_Cl_tot*(-19.746626) - mu_u_a)" + }, + { + "complexity": 7, + "loss": 2.367524, + "equation": "(Cl_tot * mu_Cl_diff) - (du_a_dt * mu_Cd_tot)", + "score": 0.021683861366975404, + "sympy_format": "Cl_tot*mu_Cl_diff - du_a_dt*mu_Cd_tot", + "lambda_format": "PySRFunction(X=>Cl_tot*mu_Cl_diff - du_a_dt*mu_Cd_tot)" + }, + { + "complexity": 8, + "loss": 1.9057635, + "equation": "(mu_Cl_tot * -19.611128) - (du_a_dt * mu_Cd_tot)", + "score": 0.21696196761590006, + "sympy_format": "-du_a_dt*mu_Cd_tot + mu_Cl_tot*(-19.611128)", + "lambda_format": "PySRFunction(X=>-du_a_dt*mu_Cd_tot + mu_Cl_tot*(-19.611128))" + }, + { + "complexity": 10, + "loss": 1.7438146, + "equation": "(mu_Cl_tot * -20.527628) - (du_a_dt * (mu_Cd_tot + mu_Cd_tot))", + "score": 0.044403851595054424, + "sympy_format": "-du_a_dt*(mu_Cd_tot + mu_Cd_tot) + mu_Cl_tot*(-20.527628)", + "lambda_format": "PySRFunction(X=>-du_a_dt*(mu_Cd_tot + mu_Cd_tot) + mu_Cl_tot*(-20.527628))" + }, + { + "complexity": 11, + "loss": 1.7022626, + "equation": "(mu_Cl_tot * -20.118673) - ((mu_Cd_tot + 0.027767241) * du_a_dt)", + "score": 0.024116705174784543, + "sympy_format": "-du_a_dt*(mu_Cd_tot + 0.027767241) + mu_Cl_tot*(-20.118673)", + "lambda_format": "PySRFunction(X=>-du_a_dt*(mu_Cd_tot + 0.027767241) + mu_Cl_tot*(-20.118673))" + }, + { + "complexity": 12, + "loss": 1.6009505, + "equation": "((mu_u_a * Cd_rear) - (du_a_dt * mu_Cd_tot)) - (mu_Cl_tot * 19.185215)", + "score": 0.061360791961067154, + "sympy_format": "Cd_rear*mu_u_a - du_a_dt*mu_Cd_tot - 19.185215*mu_Cl_tot", + "lambda_format": "PySRFunction(X=>Cd_rear*mu_u_a - du_a_dt*mu_Cd_tot - 19.185215*mu_Cl_tot)" + }, + { + "complexity": 14, + "loss": 1.4769998, + "equation": "((mu_u_a * Cd_rear) - (du_a_dt * (mu_Cd_tot + mu_Cd_tot))) - (mu_Cl_tot * 20.10174)", + "score": 0.04029232361042075, + "sympy_format": "Cd_rear*mu_u_a - du_a_dt*(mu_Cd_tot + mu_Cd_tot) - 20.10174*mu_Cl_tot", + "lambda_format": "PySRFunction(X=>Cd_rear*mu_u_a - du_a_dt*(mu_Cd_tot + mu_Cd_tot) - 20.10174*mu_Cl_tot)" + }, + { + "complexity": 15, + "loss": 1.4644551, + "equation": "((mu_u_a * Cd_rear) - (mu_Cd_tot * (du_a_dt * 1.767391))) - (mu_Cl_tot * 19.888453)", + "score": 0.008529640251924731, + "sympy_format": "Cd_rear*mu_u_a - 1.767391*du_a_dt*mu_Cd_tot - 19.888453*mu_Cl_tot", + "lambda_format": "PySRFunction(X=>Cd_rear*mu_u_a - 1.767391*du_a_dt*mu_Cd_tot - 19.888453*mu_Cl_tot)" + } + ] + }, + "top": { + "best_sympy": "3.41373730000000", + "best_score": -1.5046719425981792e-09, + "equations": [ + { + "complexity": 1, + "loss": 19.96509, + "equation": "mu_Cd_tot", + "score": 0.0, + "sympy_format": "mu_Cd_tot", + "lambda_format": "PySRFunction(X=>mu_Cd_tot)" + }, + { + "complexity": 2, + "loss": 8.361251, + "equation": "3.4137373", + "score": 0.8703771913440139, + "sympy_format": "3.41373730000000", + "lambda_format": "PySRFunction(X=>3.41373730000000)" + }, + { + "complexity": 4, + "loss": 8.297061, + "equation": "3.2585752 - mu_Cl_diff", + "score": 0.0038533506698670917, + "sympy_format": "3.2585752 - mu_Cl_diff", + "lambda_format": "PySRFunction(X=>3.2585752 - mu_Cl_diff)" + }, + { + "complexity": 5, + "loss": 8.217229, + "equation": "square(mu_Cl_diff + -1.6907136)", + "score": 0.009668308097400213, + "sympy_format": "(mu_Cl_diff - 1.6907136)**2", + "lambda_format": "PySRFunction(X=>(mu_Cl_diff - 1.6907136)**2)" + }, + { + "complexity": 6, + "loss": 7.598848, + "equation": "(du_a_dt * mu_Cd_tot) - -3.4762573", + "score": 0.07823639075701404, + "sympy_format": "du_a_dt*mu_Cd_tot - 1*(-3.4762573)", + "lambda_format": "PySRFunction(X=>du_a_dt*mu_Cd_tot - 1*(-3.4762573))" + }, + { + "complexity": 7, + "loss": 7.153428, + "equation": "square((du_a_dt * mu) + 1.8469613)", + "score": 0.06040497448521349, + "sympy_format": "(du_a_dt*mu + 1.8469613)**2", + "lambda_format": "PySRFunction(X=>(du_a_dt*mu + 1.8469613)**2)" + }, + { + "complexity": 8, + "loss": 7.122502, + "equation": "square(square((du_a_dt * mu) + 1.2848647))", + "score": 0.004332614180286218, + "sympy_format": "(du_a_dt*mu + 1.2848647)**4", + "lambda_format": "PySRFunction(X=>(du_a_dt*mu + 1.2848647)**4)" + }, + { + "complexity": 9, + "loss": 6.67839, + "equation": "1.9244562 / (0.63911325 - (mu * du_a_dt))", + "score": 0.06438212765518236, + "sympy_format": "1.9244562/(0.63911325 - du_a_dt*mu)", + "lambda_format": "PySRFunction(X=>1.9244562/(0.63911325 - du_a_dt*mu))" + }, + { + "complexity": 11, + "loss": 6.481856, + "equation": "3.4427474 - (((mu_Cl_diff + 0.06348195) - mu_Cd_tot) * du_a_dt)", + "score": 0.014935025714011268, + "sympy_format": "3.4427474 - du_a_dt*(-mu_Cd_tot + mu_Cl_diff + 0.06348195)", + "lambda_format": "PySRFunction(X=>3.4427474 - du_a_dt*(-mu_Cd_tot + mu_Cl_diff + 0.06348195))" + }, + { + "complexity": 13, + "loss": 6.3085527, + "equation": "3.3973706 - ((mu_Cl_tot + ((mu_Cl_diff + 0.062193025) - mu_Cd_tot)) * du_a_dt)", + "score": 0.013550302473921503, + "sympy_format": "3.3973706 - du_a_dt*(-mu_Cd_tot + mu_Cl_diff + mu_Cl_tot + 0.062193025)", + "lambda_format": "PySRFunction(X=>3.3973706 - du_a_dt*(-mu_Cd_tot + mu_Cl_diff + mu_Cl_tot + 0.062193025))" + }, + { + "complexity": 15, + "loss": 6.1129007, + "equation": "3.4613087 - (((mu_Cl_diff + 0.075835764) + ((mu_Cl_tot - mu_Cd_tot) - mu_Cd_tot)) * du_a_dt)", + "score": 0.01575243865724128, + "sympy_format": "3.4613087 - du_a_dt*(-mu_Cd_tot - mu_Cd_tot + mu_Cl_diff + mu_Cl_tot + 0.075835764)", + "lambda_format": "PySRFunction(X=>3.4613087 - du_a_dt*(-mu_Cd_tot - mu_Cd_tot + mu_Cl_diff + mu_Cl_tot + 0.075835764))" + } + ] + } +} \ No newline at end of file diff --git a/src/SR_analysis/validate/results/archive/pysr_karman_joint_phasemu.json b/src/SR_analysis/validate/results/archive/pysr_karman_joint_phasemu.json new file mode 100644 index 0000000..7f5ad4f --- /dev/null +++ b/src/SR_analysis/validate/results/archive/pysr_karman_joint_phasemu.json @@ -0,0 +1,243 @@ +{ + "joint": true, + "scenes": [ + "karman_re50", + "karman_re100", + "karman_re200", + "karman_re400" + ], + "label": "karman_joint_phasemu", + "front_keys": [ + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "rear_keys": [ + "u_a", + "du_a_dt", + "Cl_tot", + "dCl_tot_dt", + "Cd_tot", + "Cd_rear", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "n_samples": 742, + "front": { + "best_sympy": "-du_a_dt*mu_Cd_tot + mu_Cl_tot*(-19.60838)", + "best_score": 0.4322745824819718, + "equations": [ + { + "complexity": 1, + "loss": 3.279333, + "equation": "mu_Cl_diff", + "score": 0.0, + "sympy_format": "mu_Cl_diff", + "lambda_format": "PySRFunction(X=>mu_Cl_diff)" + }, + { + "complexity": 3, + "loss": 2.955941, + "equation": "mu_Cl_diff * Cl_tot", + "score": 0.051911502288096975, + "sympy_format": "Cl_tot*mu_Cl_diff", + "lambda_format": "PySRFunction(X=>Cl_tot*mu_Cl_diff)" + }, + { + "complexity": 4, + "loss": 2.5313113, + "equation": "mu_Cl_tot / -0.053482052", + "score": 0.1550795746187338, + "sympy_format": "mu_Cl_tot/(-0.053482052)", + "lambda_format": "PySRFunction(X=>mu_Cl_tot/(-0.053482052))" + }, + { + "complexity": 6, + "loss": 2.4194217, + "equation": "(mu_Cl_tot / -0.050641496) - mu_u_a", + "score": 0.022604462097066104, + "sympy_format": "mu_Cl_tot/(-0.050641496) - mu_u_a", + "lambda_format": "PySRFunction(X=>mu_Cl_tot/(-0.050641496) - mu_u_a)" + }, + { + "complexity": 7, + "loss": 2.367524, + "equation": "(Cl_tot * mu_Cl_diff) - (mu_Cd_tot * du_a_dt)", + "score": 0.021683861366975404, + "sympy_format": "Cl_tot*mu_Cl_diff - du_a_dt*mu_Cd_tot", + "lambda_format": "PySRFunction(X=>Cl_tot*mu_Cl_diff - du_a_dt*mu_Cd_tot)" + }, + { + "complexity": 8, + "loss": 1.9057635, + "equation": "(mu_Cl_tot * -19.60838) - (mu_Cd_tot * du_a_dt)", + "score": 0.21696196761590006, + "sympy_format": "-du_a_dt*mu_Cd_tot + mu_Cl_tot*(-19.60838)", + "lambda_format": "PySRFunction(X=>-du_a_dt*mu_Cd_tot + mu_Cl_tot*(-19.60838))" + }, + { + "complexity": 10, + "loss": 1.7438147, + "equation": "(mu_Cl_tot * -20.528954) - (du_a_dt * (mu_Cd_tot + mu_Cd_tot))", + "score": 0.044403822922282385, + "sympy_format": "-du_a_dt*(mu_Cd_tot + mu_Cd_tot) + mu_Cl_tot*(-20.528954)", + "lambda_format": "PySRFunction(X=>-du_a_dt*(mu_Cd_tot + mu_Cd_tot) + mu_Cl_tot*(-20.528954))" + }, + { + "complexity": 11, + "loss": 1.7022626, + "equation": "(mu_Cl_tot * -20.119005) - (du_a_dt * (mu_Cd_tot + 0.027783979))", + "score": 0.02411676252032859, + "sympy_format": "-du_a_dt*(mu_Cd_tot + 0.027783979) + mu_Cl_tot*(-20.119005)", + "lambda_format": "PySRFunction(X=>-du_a_dt*(mu_Cd_tot + 0.027783979) + mu_Cl_tot*(-20.119005))" + }, + { + "complexity": 12, + "loss": 1.6009505, + "equation": "((mu_Cl_tot * -19.185326) + (mu_u_a * Cd_rear)) - (du_a_dt * mu_Cd_tot)", + "score": 0.061360791961067154, + "sympy_format": "Cd_rear*mu_u_a - du_a_dt*mu_Cd_tot + mu_Cl_tot*(-19.185326)", + "lambda_format": "PySRFunction(X=>Cd_rear*mu_u_a - du_a_dt*mu_Cd_tot + mu_Cl_tot*(-19.185326))" + }, + { + "complexity": 14, + "loss": 1.5093025, + "equation": "((mu_Cl_tot * -19.185326) - ((mu_Cd_tot + mu) * du_a_dt)) + (Cd_rear * mu_u_a)", + "score": 0.029474945890791072, + "sympy_format": "Cd_rear*mu_u_a - du_a_dt*(mu + mu_Cd_tot) + mu_Cl_tot*(-19.185326)", + "lambda_format": "PySRFunction(X=>Cd_rear*mu_u_a - du_a_dt*(mu + mu_Cd_tot) + mu_Cl_tot*(-19.185326))" + } + ] + }, + "top": { + "best_sympy": "3.41382960000000", + "best_score": -4.7195136687605554e-11, + "equations": [ + { + "complexity": 1, + "loss": 14.187922, + "equation": "bias", + "score": 0.0, + "sympy_format": "bias", + "lambda_format": "PySRFunction(X=>bias)" + }, + { + "complexity": 2, + "loss": 8.361251, + "equation": "3.4138296", + "score": 0.5287829822428393, + "sympy_format": "3.41382960000000", + "lambda_format": "PySRFunction(X=>3.41382960000000)" + }, + { + "complexity": 4, + "loss": 8.297059, + "equation": "3.2583616 - mu_Cl_diff", + "score": 0.00385347119448653, + "sympy_format": "3.2583616 - mu_Cl_diff", + "lambda_format": "PySRFunction(X=>3.2583616 - mu_Cl_diff)" + }, + { + "complexity": 5, + "loss": 8.217229, + "equation": "square(1.690678 - mu_Cl_diff)", + "score": 0.009668067048161283, + "sympy_format": "(1.690678 - mu_Cl_diff)**2", + "lambda_format": "PySRFunction(X=>(1.690678 - mu_Cl_diff)**2)" + }, + { + "complexity": 6, + "loss": 7.598848, + "equation": "(mu_Cd_tot * du_a_dt) + 3.476264", + "score": 0.07823639075701404, + "sympy_format": "du_a_dt*mu_Cd_tot + 3.476264", + "lambda_format": "PySRFunction(X=>du_a_dt*mu_Cd_tot + 3.476264)" + }, + { + "complexity": 7, + "loss": 7.153428, + "equation": "square((mu * du_a_dt) + 1.847013)", + "score": 0.06040497448521349, + "sympy_format": "(du_a_dt*mu + 1.847013)**2", + "lambda_format": "PySRFunction(X=>(du_a_dt*mu + 1.847013)**2)" + }, + { + "complexity": 8, + "loss": 7.1225033, + "equation": "square(square((du_a_dt * mu) + 1.2847948))", + "score": 0.004332431660171536, + "sympy_format": "(du_a_dt*mu + 1.2847948)**4", + "lambda_format": "PySRFunction(X=>(du_a_dt*mu + 1.2847948)**4)" + }, + { + "complexity": 9, + "loss": 6.942499, + "equation": "3.3788114 - ((mu_Cl_diff - -0.04983927) * du_a_dt)", + "score": 0.025597454551806478, + "sympy_format": "3.3788114 - du_a_dt*(mu_Cl_diff - 1*(-0.04983927))", + "lambda_format": "PySRFunction(X=>3.3788114 - du_a_dt*(mu_Cl_diff - 1*(-0.04983927)))" + }, + { + "complexity": 10, + "loss": 6.7540913, + "equation": "square(square((du_a_dt * mu) + 1.1830567)) + bias", + "score": 0.0275133563711567, + "sympy_format": "bias + (du_a_dt*mu + 1.1830567)**4", + "lambda_format": "PySRFunction(X=>bias + (du_a_dt*mu + 1.1830567)**4)" + }, + { + "complexity": 11, + "loss": 6.7152443, + "equation": "(3.3716245 - (du_a_dt * mu_Cl_diff)) - (u_a / 17.041)", + "score": 0.005768229237860644, + "sympy_format": "-du_a_dt*mu_Cl_diff - u_a/17.041 + 3.3716245", + "lambda_format": "PySRFunction(X=>-du_a_dt*mu_Cl_diff - u_a/17.041 + 3.3716245)" + }, + { + "complexity": 12, + "loss": 6.6502523, + "equation": "(square(square((du_a_dt * mu) + 1.1830306)) + bias) - mu_u_a", + "score": 0.009725416751769576, + "sympy_format": "bias - mu_u_a + (du_a_dt*mu + 1.1830306)**4", + "lambda_format": "PySRFunction(X=>bias - mu_u_a + (du_a_dt*mu + 1.1830306)**4)" + }, + { + "complexity": 13, + "loss": 6.3269286, + "equation": "(3.3770745 - (mu_Cl_diff * du_a_dt)) - ((u_a + du_a_dt) / 22.185211)", + "score": 0.04983988866579723, + "sympy_format": "-du_a_dt*mu_Cl_diff - (du_a_dt + u_a)/22.185211 + 3.3770745", + "lambda_format": "PySRFunction(X=>-du_a_dt*mu_Cl_diff - (du_a_dt + u_a)/22.185211 + 3.3770745)" + }, + { + "complexity": 14, + "loss": 6.314921, + "equation": "(((-0.039736323 - mu_Cl_diff) * du_a_dt) - (u_a / 19.805904)) - -3.376313", + "score": 0.001899659304757144, + "sympy_format": "du_a_dt*(-mu_Cl_diff - 0.039736323) - u_a/19.805904 - 1*(-3.376313)", + "lambda_format": "PySRFunction(X=>du_a_dt*(-mu_Cl_diff - 0.039736323) - u_a/19.805904 - 1*(-3.376313))" + }, + { + "complexity": 15, + "loss": 6.1714354, + "equation": "mu_u_a + ((3.3808138 - (du_a_dt * mu_Cl_diff)) - ((u_a + du_a_dt) / 19.6594))", + "score": 0.022983793151513908, + "sympy_format": "-du_a_dt*mu_Cl_diff + mu_u_a - (du_a_dt + u_a)/19.6594 + 3.3808138", + "lambda_format": "PySRFunction(X=>-du_a_dt*mu_Cl_diff + mu_u_a - (du_a_dt + u_a)/19.6594 + 3.3808138)" + } + ] + } +} \ No newline at end of file diff --git a/src/SR_analysis/validate/results/archive/pysr_karman_joint_phys_dadt.json b/src/SR_analysis/validate/results/archive/pysr_karman_joint_phys_dadt.json new file mode 100644 index 0000000..e2d4300 --- /dev/null +++ b/src/SR_analysis/validate/results/archive/pysr_karman_joint_phys_dadt.json @@ -0,0 +1,209 @@ +{ + "joint": true, + "scenes": [ + "karman_re50", + "karman_re100", + "karman_re200", + "karman_re400" + ], + "label": "karman_joint_phys_dadt", + "front_keys": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "daF_dt", + "daB_dt", + "daT_dt" + ], + "rear_keys": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "daF_dt", + "daB_dt", + "daT_dt" + ], + "n_samples": 742, + "front": { + "best_sympy": "daB_dt*0.3933842 + daF_dt", + "best_score": 0.4452889889048576, + "equations": [ + { + "complexity": 1, + "loss": 11.860913, + "equation": "Cl_tot", + "score": 0.0, + "sympy_format": "Cl_tot", + "lambda_format": "PySRFunction(X=>Cl_tot)" + }, + { + "complexity": 2, + "loss": 3.35684, + "equation": "-0.20072505", + "score": 1.2622483167974863, + "sympy_format": "-0.200725050000000", + "lambda_format": "PySRFunction(X=>-0.200725050000000)" + }, + { + "complexity": 3, + "loss": 3.0562248, + "equation": "Cl_tot / Cl_diff", + "score": 0.09381962645114764, + "sympy_format": "Cl_tot/Cl_diff", + "lambda_format": "PySRFunction(X=>Cl_tot/Cl_diff)" + }, + { + "complexity": 4, + "loss": 2.6033053, + "equation": "daF_dt * 0.4842881", + "score": 0.1603985219286102, + "sympy_format": "daF_dt*0.4842881", + "lambda_format": "PySRFunction(X=>daF_dt*0.4842881)" + }, + { + "complexity": 6, + "loss": 1.8620762, + "equation": "(daB_dt * 0.3933842) + daF_dt", + "score": 0.16754490256211507, + "sympy_format": "daB_dt*0.3933842 + daF_dt", + "lambda_format": "PySRFunction(X=>daB_dt*0.3933842 + daF_dt)" + }, + { + "complexity": 8, + "loss": 1.7871327, + "equation": "((daB_dt - Cl_tot) * 0.3023147) + daF_dt", + "score": 0.02053980490066004, + "sympy_format": "daF_dt + (-Cl_tot + daB_dt)*0.3023147", + "lambda_format": "PySRFunction(X=>daF_dt + (-Cl_tot + daB_dt)*0.3023147)" + }, + { + "complexity": 10, + "loss": 1.6541386, + "equation": "daF_dt + (((daB_dt + daB_dt) - Cl_tot) * 0.1877773)", + "score": 0.03866605100363626, + "sympy_format": "daF_dt + (-Cl_tot + daB_dt + daB_dt)*0.1877773", + "lambda_format": "PySRFunction(X=>daF_dt + (-Cl_tot + daB_dt + daB_dt)*0.1877773)" + }, + { + "complexity": 11, + "loss": 1.6539634, + "equation": "daF_dt + ((daB_dt * 0.37739208) + (Cl_tot * -0.18279383))", + "score": 0.00010592176430096122, + "sympy_format": "Cl_tot*(-0.18279383) + daB_dt*0.37739208 + daF_dt", + "lambda_format": "PySRFunction(X=>Cl_tot*(-0.18279383) + daB_dt*0.37739208 + daF_dt)" + }, + { + "complexity": 12, + "loss": 1.5505577, + "equation": "daF_dt + (((daB_dt + daB_dt) - Cl_tot) * (-1.9413033 / Cl_diff))", + "score": 0.0645597955145951, + "sympy_format": "daF_dt + (-Cl_tot + daB_dt + daB_dt)*(-1.9413033)/Cl_diff", + "lambda_format": "PySRFunction(X=>daF_dt + (-Cl_tot + daB_dt + daB_dt)*(-1.9413033)/Cl_diff)" + } + ] + }, + "top": { + "best_sympy": "3.41517 - daF_dt", + "best_score": 0.5265476719318889, + "equations": [ + { + "complexity": 1, + "loss": 20.182537, + "equation": "Cd_tot", + "score": 0.0, + "sympy_format": "Cd_tot", + "lambda_format": "PySRFunction(X=>Cd_tot)" + }, + { + "complexity": 2, + "loss": 8.361251, + "equation": "3.4138327", + "score": 0.8812096685069358, + "sympy_format": "3.41383270000000", + "lambda_format": "PySRFunction(X=>3.41383270000000)" + }, + { + "complexity": 4, + "loss": 3.9586535, + "equation": "3.41517 - daF_dt", + "score": 0.37385205742882277, + "sympy_format": "3.41517 - daF_dt", + "lambda_format": "PySRFunction(X=>3.41517 - daF_dt)" + }, + { + "complexity": 6, + "loss": 3.8982775, + "equation": "(u_m * 0.034880333) - daF_dt", + "score": 0.007684576643343227, + "sympy_format": "-daF_dt + u_m*0.034880333", + "lambda_format": "PySRFunction(X=>-daF_dt + u_m*0.034880333)" + }, + { + "complexity": 7, + "loss": 3.881636, + "equation": "(daF_dt / -0.86890197) + 3.4153676", + "score": 0.004278074654929344, + "sympy_format": "daF_dt/(-0.86890197) + 3.4153676", + "lambda_format": "PySRFunction(X=>daF_dt/(-0.86890197) + 3.4153676)" + }, + { + "complexity": 8, + "loss": 3.7701073, + "equation": "((u_m + daT_dt) * 0.03503163) - daF_dt", + "score": 0.02915325165719545, + "sympy_format": "-daF_dt + (daT_dt + u_m)*0.03503163", + "lambda_format": "PySRFunction(X=>-daF_dt + (daT_dt + u_m)*0.03503163)" + }, + { + "complexity": 9, + "loss": 3.7130995, + "equation": "((daT_dt * 0.11201806) - daF_dt) + 3.4146602", + "score": 0.01523649001979041, + "sympy_format": "-daF_dt + daT_dt*0.11201806 + 3.4146602", + "lambda_format": "PySRFunction(X=>-daF_dt + daT_dt*0.11201806 + 3.4146602)" + }, + { + "complexity": 10, + "loss": 3.6897433, + "equation": "(((daT_dt + u_m) + daT_dt) * 0.035040542) - daF_dt", + "score": 0.0063100833366505895, + "sympy_format": "-daF_dt + (daT_dt + daT_dt + u_m)*0.035040542", + "lambda_format": "PySRFunction(X=>-daF_dt + (daT_dt + daT_dt + u_m)*0.035040542)" + }, + { + "complexity": 11, + "loss": 3.406545, + "equation": "(((daT_dt - daB_dt) * 0.26789883) - daF_dt) + 3.409648", + "score": 0.07985830805295381, + "sympy_format": "-daF_dt + (-daB_dt + daT_dt)*0.26789883 + 3.409648", + "lambda_format": "PySRFunction(X=>-daF_dt + (-daB_dt + daT_dt)*0.26789883 + 3.409648)" + }, + { + "complexity": 13, + "loss": 3.3150167, + "equation": "((((daT_dt - Cd_tot) - daB_dt) * 0.28851545) + 3.6058447) - daF_dt", + "score": 0.013617959925718374, + "sympy_format": "-daF_dt + (-Cd_tot - daB_dt + daT_dt)*0.28851545 + 3.6058447", + "lambda_format": "PySRFunction(X=>-daF_dt + (-Cd_tot - daB_dt + daT_dt)*0.28851545 + 3.6058447)" + }, + { + "complexity": 15, + "loss": 3.3150165, + "equation": "((bias * ((daT_dt - Cd_tot) - daB_dt)) * 0.28849953) + (3.6058931 - daF_dt)", + "score": 3.016576146611765e-08, + "sympy_format": "bias*(-Cd_tot - daB_dt + daT_dt)*0.28849953 - daF_dt + 3.6058931", + "lambda_format": "PySRFunction(X=>bias*(-Cd_tot - daB_dt + daT_dt)*0.28849953 - daF_dt + 3.6058931)" + } + ] + } +} \ No newline at end of file diff --git a/src/SR_analysis/validate/results/archive/pysr_karman_joint_top.json b/src/SR_analysis/validate/results/archive/pysr_karman_joint_top.json new file mode 100644 index 0000000..6f24f2a --- /dev/null +++ b/src/SR_analysis/validate/results/archive/pysr_karman_joint_top.json @@ -0,0 +1,31 @@ +{ + "scene": "karman_joint", + "scene_id": "karman", + "channel": "top", + "output": "alpha", + "feature_names": [ + "bias", + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "best_sympy": "bias + (aT_lag1 + daT)/1.4642353", + "best_score": 0.7725416952339563 +} \ No newline at end of file diff --git a/src/SR_analysis/validate/results/archive/pysr_karman_joint_v2_full.json b/src/SR_analysis/validate/results/archive/pysr_karman_joint_v2_full.json new file mode 100644 index 0000000..3f46e52 --- /dev/null +++ b/src/SR_analysis/validate/results/archive/pysr_karman_joint_v2_full.json @@ -0,0 +1,235 @@ +{ + "joint": true, + "scenes": [ + "karman_re50", + "karman_re100", + "karman_re200", + "karman_re400" + ], + "label": "karman_joint_v2_full", + "front_keys": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "rear_keys": [ + "u_m", + "u_a", + "u_c", + "v_a", + "Cd_tot", + "Cd_rear", + "Cl_tot", + "Cl_diff", + "aF_lag1", + "aB_lag1", + "aT_lag1", + "daF", + "daB", + "daT", + "mu", + "mu_u_a", + "mu_v_a", + "mu_Cd_tot", + "mu_Cl_diff", + "mu_Cl_tot" + ], + "n_samples": 742, + "front": { + "best_sympy": "(aF_lag1 + daF)*0.8435828", + "best_score": 0.9378936963643454, + "equations": [ + { + "complexity": 1, + "loss": 0.54427725, + "equation": "aF_lag1", + "score": 0.0, + "sympy_format": "aF_lag1", + "lambda_format": "PySRFunction(X=>aF_lag1)" + }, + { + "complexity": 3, + "loss": 0.3181017, + "equation": "daF + aF_lag1", + "score": 0.26854381239560915, + "sympy_format": "aF_lag1 + daF", + "lambda_format": "PySRFunction(X=>aF_lag1 + daF)" + }, + { + "complexity": 5, + "loss": 0.31256455, + "equation": "(mu_Cd_tot + daF) + aF_lag1", + "score": 0.008780067563157546, + "sympy_format": "aF_lag1 + daF + mu_Cd_tot", + "lambda_format": "PySRFunction(X=>aF_lag1 + daF + mu_Cd_tot)" + }, + { + "complexity": 6, + "loss": 0.20848092, + "equation": "(aF_lag1 + daF) * 0.8435828", + "score": 0.4049634815857554, + "sympy_format": "(aF_lag1 + daF)*0.8435828", + "lambda_format": "PySRFunction(X=>(aF_lag1 + daF)*0.8435828)" + }, + { + "complexity": 8, + "loss": 0.19918622, + "equation": "((daF + aF_lag1) * 0.8328231) - mu_Cl_tot", + "score": 0.0228036801046347, + "sympy_format": "-mu_Cl_tot + (aF_lag1 + daF)*0.8328231", + "lambda_format": "PySRFunction(X=>-mu_Cl_tot + (aF_lag1 + daF)*0.8328231)" + }, + { + "complexity": 9, + "loss": 0.19748373, + "equation": "(aF_lag1 + daF) * (square(mu_Cl_diff) + 0.8138635)", + "score": 0.008583964849908426, + "sympy_format": "(aF_lag1 + daF)*(mu_Cl_diff**2 + 0.8138635)", + "lambda_format": "PySRFunction(X=>(aF_lag1 + daF)*(mu_Cl_diff**2 + 0.8138635))" + }, + { + "complexity": 10, + "loss": 0.18920267, + "equation": "((aF_lag1 + daF) - (mu_Cl_tot * aT_lag1)) * 0.81562567", + "score": 0.04283743263199969, + "sympy_format": "(aF_lag1 - aT_lag1*mu_Cl_tot + daF)*0.81562567", + "lambda_format": "PySRFunction(X=>(aF_lag1 - aT_lag1*mu_Cl_tot + daF)*0.81562567)" + }, + { + "complexity": 12, + "loss": 0.18285154, + "equation": "(((aT_lag1 * (mu_Cd_tot - mu_Cl_tot)) + aF_lag1) + daF) * 0.8419187", + "score": 0.01707210090251386, + "sympy_format": "(aF_lag1 + aT_lag1*(mu_Cd_tot - mu_Cl_tot) + daF)*0.8419187", + "lambda_format": "PySRFunction(X=>(aF_lag1 + aT_lag1*(mu_Cd_tot - mu_Cl_tot) + daF)*0.8419187)" + }, + { + "complexity": 13, + "loss": 0.17009866, + "equation": "((mu_Cd_tot * daB) * 5.15918) + ((daF + aF_lag1) * 0.8465594)", + "score": 0.07229594511211809, + "sympy_format": "mu_Cd_tot*daB*5.15918 + (aF_lag1 + daF)*0.8465594", + "lambda_format": "PySRFunction(X=>mu_Cd_tot*daB*5.15918 + (aF_lag1 + daF)*0.8465594)" + }, + { + "complexity": 15, + "loss": 0.15994309, + "equation": "((aF_lag1 + daF) * 0.80574006) + (((mu_Cd_tot * daB) - mu_Cl_tot) * 3.7148018)", + "score": 0.030780278594886763, + "sympy_format": "(aF_lag1 + daF)*0.80574006 + (daB*mu_Cd_tot - mu_Cl_tot)*3.7148018", + "lambda_format": "PySRFunction(X=>(aF_lag1 + daF)*0.80574006 + (daB*mu_Cd_tot - mu_Cl_tot)*3.7148018)" + } + ] + }, + "top": { + "best_sympy": "bias + (aT_lag1 + daT)/1.4642353", + "best_score": 0.7725416952339563, + "equations": [ + { + "complexity": 1, + "loss": 3.1322763, + "equation": "aT_lag1", + "score": 0.0, + "sympy_format": "aT_lag1", + "lambda_format": "PySRFunction(X=>aT_lag1)" + }, + { + "complexity": 3, + "loss": 2.7126927, + "equation": "aT_lag1 + daB", + "score": 0.07190911740522084, + "sympy_format": "aT_lag1 + daB", + "lambda_format": "PySRFunction(X=>aT_lag1 + daB)" + }, + { + "complexity": 5, + "loss": 2.476772, + "equation": "(daB + aT_lag1) + daF", + "score": 0.0454928291890485, + "sympy_format": "aT_lag1 + daB + daF", + "lambda_format": "PySRFunction(X=>aT_lag1 + daB + daF)" + }, + { + "complexity": 6, + "loss": 2.3566358, + "equation": "(daB + aT_lag1) * 0.8755936", + "score": 0.04972100588068573, + "sympy_format": "(aT_lag1 + daB)*0.8755936", + "lambda_format": "PySRFunction(X=>(aT_lag1 + daB)*0.8755936)" + }, + { + "complexity": 8, + "loss": 1.901836, + "equation": "bias + ((aT_lag1 + daT) / 1.4642353)", + "score": 0.10720767908990216, + "sympy_format": "bias + (aT_lag1 + daT)/1.4642353", + "lambda_format": "PySRFunction(X=>bias + (aT_lag1 + daT)/1.4642353)" + }, + { + "complexity": 9, + "loss": 1.8924239, + "equation": "(daT + (aT_lag1 + 1.6976935)) / 1.5016745", + "score": 0.004961241218984858, + "sympy_format": "(aT_lag1 + daT + 1.6976935)/1.5016745", + "lambda_format": "PySRFunction(X=>(aT_lag1 + daT + 1.6976935)/1.5016745)" + }, + { + "complexity": 10, + "loss": 1.8681406, + "equation": "(bias + ((daT + aT_lag1) / 1.5115219)) - mu_Cl_diff", + "score": 0.012914889520199146, + "sympy_format": "bias - mu_Cl_diff + (aT_lag1 + daT)/1.5115219", + "lambda_format": "PySRFunction(X=>bias - mu_Cl_diff + (aT_lag1 + daT)/1.5115219)" + }, + { + "complexity": 11, + "loss": 1.7923925, + "equation": "(((aT_lag1 + daT) + 2.4296286) - daF) / 1.7158659", + "score": 0.04139228505289259, + "sympy_format": "(aT_lag1 - daF + daT + 2.4296286)/1.7158659", + "lambda_format": "PySRFunction(X=>(aT_lag1 - daF + daT + 2.4296286)/1.7158659)" + }, + { + "complexity": 13, + "loss": 1.7680607, + "equation": "(daT * 0.42485157) + (((aT_lag1 - daF) / 1.4859257) + bias)", + "score": 0.006834011690977459, + "sympy_format": "bias + daT*0.42485157 + (aT_lag1 - daF)/1.4859257", + "lambda_format": "PySRFunction(X=>bias + daT*0.42485157 + (aT_lag1 - daF)/1.4859257)" + }, + { + "complexity": 14, + "loss": 1.7424377, + "equation": "(((aT_lag1 + 1.953018) - daF) / 1.5761796) + (daT * 0.45442566)", + "score": 0.014598186420028183, + "sympy_format": "daT*0.45442566 + (aT_lag1 - daF + 1.953018)/1.5761796", + "lambda_format": "PySRFunction(X=>daT*0.45442566 + (aT_lag1 - daF + 1.953018)/1.5761796)" + }, + { + "complexity": 15, + "loss": 1.71649, + "equation": "(daT * 0.44305515) + (bias + (((aT_lag1 - daF) / 1.5455538) - mu_Cl_diff))", + "score": 0.015003601694950701, + "sympy_format": "bias + daT*0.44305515 - mu_Cl_diff + (aT_lag1 - daF)/1.5455538", + "lambda_format": "PySRFunction(X=>bias + daT*0.44305515 - mu_Cl_diff + (aT_lag1 - daF)/1.5455538)" + } + ] + } +} \ No newline at end of file diff --git a/src/SR_analysis/validate/results/archive/pysr_karman_joint_whitelist.json b/src/SR_analysis/validate/results/archive/pysr_karman_joint_whitelist.json new file mode 100644 index 0000000..fabf613 --- /dev/null +++ b/src/SR_analysis/validate/results/archive/pysr_karman_joint_whitelist.json @@ -0,0 +1,210 @@ +{ + "joint": true, + "scenes": [ + "karman_re50", + "karman_re100", + "karman_re200", + "karman_re400" + ], + "label": "karman_joint_whitelist", + "front_keys": [ + "daF_dt", + "mu", + "mu_u_a", + "mu_Cd_tot", + "mu_Cl_diff" + ], + "rear_keys": [ + "u_a", + "Cd_rear", + "Cl_diff", + "daF_dt", + "daB_dt", + "daT_dt", + "mu", + "mu_u_a", + "mu_Cd_tot", + "mu_Cl_diff" + ], + "n_samples": 742, + "front": { + "best_sympy": "daF_dt*0.48772675 + (3043317.54988146*mu**4)*mu_u_a - mu_u_a", + "best_score": 0.49454893657247134, + "equations": [ + { + "complexity": 1, + "loss": 3.279333, + "equation": "mu_Cl_diff", + "score": 0.0, + "sympy_format": "mu_Cl_diff", + "lambda_format": "PySRFunction(X=>mu_Cl_diff)" + }, + { + "complexity": 3, + "loss": 3.2266438, + "equation": "mu_Cl_diff + mu_Cl_diff", + "score": 0.0080987611199759, + "sympy_format": "mu_Cl_diff + mu_Cl_diff", + "lambda_format": "PySRFunction(X=>mu_Cl_diff + mu_Cl_diff)" + }, + { + "complexity": 4, + "loss": 2.6033056, + "equation": "daF_dt * 0.48450768", + "score": 0.21466050370062678, + "sympy_format": "daF_dt*0.48450768", + "lambda_format": "PySRFunction(X=>daF_dt*0.48450768)" + }, + { + "complexity": 6, + "loss": 2.4827878, + "equation": "mu_Cl_diff + (daF_dt * 0.4852985)", + "score": 0.02369999018974129, + "sympy_format": "daF_dt*0.4852985 + mu_Cl_diff", + "lambda_format": "PySRFunction(X=>daF_dt*0.4852985 + mu_Cl_diff)" + }, + { + "complexity": 8, + "loss": 2.426514, + "equation": "((daF_dt * 0.4973827) + mu_Cl_diff) - mu_u_a", + "score": 0.011463191132385675, + "sympy_format": "daF_dt*0.4973827 + mu_Cl_diff - mu_u_a", + "lambda_format": "PySRFunction(X=>daF_dt*0.4973827 + mu_Cl_diff - mu_u_a)" + }, + { + "complexity": 9, + "loss": 2.4233806, + "equation": "(mu_Cl_diff + (daF_dt * 0.20695291)) / 0.42549562", + "score": 0.0012921519721171505, + "sympy_format": "(daF_dt*0.20695291 + mu_Cl_diff)/0.42549562", + "lambda_format": "PySRFunction(X=>(daF_dt*0.20695291 + mu_Cl_diff)/0.42549562)" + }, + { + "complexity": 10, + "loss": 2.3438127, + "equation": "(square(mu * 85.03868) + -5.492055) * mu_u_a", + "score": 0.033384545122651786, + "sympy_format": "mu_u_a*(7231.5770961424*mu**2 - 5.492055)", + "lambda_format": "PySRFunction(X=>mu_u_a*(7231.5770961424*mu**2 - 5.492055))" + }, + { + "complexity": 11, + "loss": 2.196233, + "equation": "(square(square(mu * 45.730694)) + -3.9295652) * mu_u_a", + "score": 0.06503534235624786, + "sympy_format": "mu_u_a*(4373520.52274126*mu**4 - 3.9295652)", + "lambda_format": "PySRFunction(X=>mu_u_a*(4373520.52274126*mu**4 - 3.9295652))" + }, + { + "complexity": 13, + "loss": 1.9753566, + "equation": "(mu_u_a * square(square(mu * 40.187386))) + (daF_dt * 0.47694647)", + "score": 0.0529973405029218, + "sympy_format": "daF_dt*0.47694647 + (2608308.95972264*mu**4)*mu_u_a", + "lambda_format": "PySRFunction(X=>daF_dt*0.47694647 + (2608308.95972264*mu**4)*mu_u_a)" + }, + { + "complexity": 15, + "loss": 1.6967186, + "equation": "(daF_dt * 0.48772675) + ((square(square(mu * 41.76734)) * mu_u_a) - mu_u_a)", + "score": 0.07602639427673068, + "sympy_format": "daF_dt*0.48772675 + (3043317.54988146*mu**4)*mu_u_a - mu_u_a", + "lambda_format": "PySRFunction(X=>daF_dt*0.48772675 + (3043317.54988146*mu**4)*mu_u_a - mu_u_a)" + } + ] + }, + "top": { + "best_sympy": "3.41517 - daF_dt", + "best_score": 0.5265476719318889, + "equations": [ + { + "complexity": 1, + "loss": 14.187922, + "equation": "bias", + "score": 0.0, + "sympy_format": "bias", + "lambda_format": "PySRFunction(X=>bias)" + }, + { + "complexity": 2, + "loss": 8.361251, + "equation": "3.4138322", + "score": 0.5287829822428393, + "sympy_format": "3.41383220000000", + "lambda_format": "PySRFunction(X=>3.41383220000000)" + }, + { + "complexity": 4, + "loss": 3.9586535, + "equation": "3.41517 - daF_dt", + "score": 0.37385205742882277, + "sympy_format": "3.41517 - daF_dt", + "lambda_format": "PySRFunction(X=>3.41517 - daF_dt)" + }, + { + "complexity": 6, + "loss": 3.889291, + "equation": "(3.2598698 - daF_dt) - mu_Cl_diff", + "score": 0.00883853169219368, + "sympy_format": "-daF_dt - mu_Cl_diff + 3.2598698", + "lambda_format": "PySRFunction(X=>-daF_dt - mu_Cl_diff + 3.2598698)" + }, + { + "complexity": 7, + "loss": 3.7928388, + "equation": "square(mu_Cl_diff + -1.691413) - daF_dt", + "score": 0.025112116345034522, + "sympy_format": "-daF_dt + (mu_Cl_diff - 1.691413)**2", + "lambda_format": "PySRFunction(X=>-daF_dt + (mu_Cl_diff - 1.691413)**2)" + }, + { + "complexity": 8, + "loss": 3.625897, + "equation": "3.4101334 - (daF_dt + (daT_dt * mu_Cl_diff))", + "score": 0.04501305649052308, + "sympy_format": "3.4101334 - (daF_dt + daT_dt*mu_Cl_diff)", + "lambda_format": "PySRFunction(X=>3.4101334 - (daF_dt + daT_dt*mu_Cl_diff))" + }, + { + "complexity": 9, + "loss": 3.5895607, + "equation": "4.1869073 - (daF_dt - (0.07591406 / mu_Cl_diff))", + "score": 0.010071878647596271, + "sympy_format": "4.1869073 - (daF_dt - 0.07591406/mu_Cl_diff)", + "lambda_format": "PySRFunction(X=>4.1869073 - (daF_dt - 0.07591406/mu_Cl_diff))" + }, + { + "complexity": 10, + "loss": 3.358361, + "equation": "3.4111354 - (square(mu_Cl_diff + 1.2137802) * daF_dt)", + "score": 0.06657676998052625, + "sympy_format": "3.4111354 - daF_dt*(mu_Cl_diff + 1.2137802)**2", + "lambda_format": "PySRFunction(X=>3.4111354 - daF_dt*(mu_Cl_diff + 1.2137802)**2)" + }, + { + "complexity": 11, + "loss": 3.0340853, + "equation": "3.4122708 - (daF_dt * square(square(mu_Cl_diff + 1.1701599)))", + "score": 0.1015430621129069, + "sympy_format": "3.4122708 - daF_dt*(mu_Cl_diff + 1.1701599)**4", + "lambda_format": "PySRFunction(X=>3.4122708 - daF_dt*(mu_Cl_diff + 1.1701599)**4)" + }, + { + "complexity": 12, + "loss": 2.7559764, + "equation": "3.3953567 - (((mu_Cl_diff * 7.6420603) + 2.1442635) * daF_dt)", + "score": 0.09613820525142552, + "sympy_format": "3.3953567 - daF_dt*(mu_Cl_diff*7.6420603 + 2.1442635)", + "lambda_format": "PySRFunction(X=>3.3953567 - daF_dt*(mu_Cl_diff*7.6420603 + 2.1442635))" + }, + { + "complexity": 14, + "loss": 2.7082841, + "equation": "3.2556021 - (mu_Cl_diff + (daF_dt * (1.935907 - (mu * 53.38167))))", + "score": 0.008728264529423635, + "sympy_format": "3.2556021 - (daF_dt*(1.935907 - 53.38167*mu) + mu_Cl_diff)", + "lambda_format": "PySRFunction(X=>3.2556021 - (daF_dt*(1.935907 - 53.38167*mu) + mu_Cl_diff))" + } + ] + } +} \ No newline at end of file diff --git a/src/SR_analysis/validate/results/archive/pysr_karman_top.json b/src/SR_analysis/validate/results/archive/pysr_karman_top.json new file mode 100644 index 0000000..9d60b25 --- /dev/null +++ b/src/SR_analysis/validate/results/archive/pysr_karman_top.json @@ -0,0 +1,49 @@ +{ + "scene": "karman_re100", + "channel": "top", + "feature_names": [ + "bias", + "u_a", + "Cd_rear", + "Cl_diff", + "aF_lag1", + "aB_lag1", + "aT_lag1" + ], + "equations": [ + { + "complexity": 1, + "loss": 0.91968936, + "equation": "bias", + "score": 0.0, + "sympy_format": "bias", + "lambda_format": "PySRFunction(X=>bias)" + }, + { + "complexity": 2, + "loss": 0.00031669735, + "equation": "0.041160837", + "score": 7.973844653864653, + "sympy_format": "0.0411608370000000", + "lambda_format": "PySRFunction(X=>0.0411608370000000)" + }, + { + "complexity": 4, + "loss": 1.6611292e-17, + "equation": "aT_lag1 * 0.010000001", + "score": 15.28944249841527, + "sympy_format": "aT_lag1*0.010000001", + "lambda_format": "PySRFunction(X=>aT_lag1*0.010000001)" + }, + { + "complexity": 7, + "loss": 7.429523e-18, + "equation": "(aT_lag1 - 4.1009116e-7) * 0.010000001", + "score": 0.2682070158636629, + "sympy_format": "(aT_lag1 - 1*4.1009116e-7)*0.010000001", + "lambda_format": "PySRFunction(X=>(aT_lag1 - 1*4.1009116e-7)*0.010000001)" + } + ], + "best_sympy": "(aT_lag1 - 1*4.1009116e-7)*0.010000001", + "best_score": 0.99999999999999 +} \ No newline at end of file diff --git a/src/SR_analysis/validate/results/illusion_0.75L_PPO_vorticity.png b/src/SR_analysis/validate/results/illusion_0.75L_PPO_vorticity.png new file mode 100644 index 0000000..05721cb Binary files /dev/null and b/src/SR_analysis/validate/results/illusion_0.75L_PPO_vorticity.png differ diff --git a/src/SR_analysis/validate/results/illusion_0.75L_target.png b/src/SR_analysis/validate/results/illusion_0.75L_target.png new file mode 100644 index 0000000..9e91d6c Binary files /dev/null and b/src/SR_analysis/validate/results/illusion_0.75L_target.png differ diff --git a/src/SR_analysis/validate/results/illusion_0.75L_target_vorticity.png b/src/SR_analysis/validate/results/illusion_0.75L_target_vorticity.png new file mode 100644 index 0000000..17840f7 Binary files /dev/null and b/src/SR_analysis/validate/results/illusion_0.75L_target_vorticity.png differ diff --git a/src/SR_analysis/validate/results/illusion_0.75L_uncontrolled.png b/src/SR_analysis/validate/results/illusion_0.75L_uncontrolled.png new file mode 100644 index 0000000..610998b Binary files /dev/null and b/src/SR_analysis/validate/results/illusion_0.75L_uncontrolled.png differ diff --git a/src/SR_analysis/validate/results/illusion_0.75L_uncontrolled_vorticity.png b/src/SR_analysis/validate/results/illusion_0.75L_uncontrolled_vorticity.png new file mode 100644 index 0000000..f56354b Binary files /dev/null and b/src/SR_analysis/validate/results/illusion_0.75L_uncontrolled_vorticity.png differ diff --git a/src/SR_analysis/validate/results/illusion_0.75L_vorticity.png b/src/SR_analysis/validate/results/illusion_0.75L_vorticity.png new file mode 100644 index 0000000..0754c2a Binary files /dev/null and b/src/SR_analysis/validate/results/illusion_0.75L_vorticity.png differ diff --git a/src/SR_analysis/validate/results/illusion_1.5L_PPO_vorticity.png b/src/SR_analysis/validate/results/illusion_1.5L_PPO_vorticity.png new file mode 100644 index 0000000..5a4f40b Binary files /dev/null and b/src/SR_analysis/validate/results/illusion_1.5L_PPO_vorticity.png differ diff --git a/src/SR_analysis/validate/results/illusion_1.5L_target.png b/src/SR_analysis/validate/results/illusion_1.5L_target.png new file mode 100644 index 0000000..7e7f574 Binary files /dev/null and b/src/SR_analysis/validate/results/illusion_1.5L_target.png differ diff --git a/src/SR_analysis/validate/results/illusion_1.5L_target_vorticity.png b/src/SR_analysis/validate/results/illusion_1.5L_target_vorticity.png new file mode 100644 index 0000000..a48bc18 Binary files /dev/null and b/src/SR_analysis/validate/results/illusion_1.5L_target_vorticity.png differ diff --git a/src/SR_analysis/validate/results/illusion_1.5L_uncontrolled.png b/src/SR_analysis/validate/results/illusion_1.5L_uncontrolled.png new file mode 100644 index 0000000..bb9985e Binary files /dev/null and b/src/SR_analysis/validate/results/illusion_1.5L_uncontrolled.png differ diff --git a/src/SR_analysis/validate/results/illusion_1.5L_uncontrolled_vorticity.png b/src/SR_analysis/validate/results/illusion_1.5L_uncontrolled_vorticity.png new file mode 100644 index 0000000..b086674 Binary files /dev/null and b/src/SR_analysis/validate/results/illusion_1.5L_uncontrolled_vorticity.png differ diff --git a/src/SR_analysis/validate/results/illusion_1.5L_vorticity.png b/src/SR_analysis/validate/results/illusion_1.5L_vorticity.png new file mode 100644 index 0000000..569c898 Binary files /dev/null and b/src/SR_analysis/validate/results/illusion_1.5L_vorticity.png differ diff --git a/src/SR_analysis/validate/results/illusion_1L_PPO_vorticity.png b/src/SR_analysis/validate/results/illusion_1L_PPO_vorticity.png new file mode 100644 index 0000000..fb80df0 Binary files /dev/null and b/src/SR_analysis/validate/results/illusion_1L_PPO_vorticity.png differ diff --git a/src/SR_analysis/validate/results/illusion_1L_target.png b/src/SR_analysis/validate/results/illusion_1L_target.png new file mode 100644 index 0000000..9095526 Binary files /dev/null and b/src/SR_analysis/validate/results/illusion_1L_target.png differ diff --git a/src/SR_analysis/validate/results/illusion_1L_target_vorticity.png b/src/SR_analysis/validate/results/illusion_1L_target_vorticity.png new file mode 100644 index 0000000..568b018 Binary files /dev/null and b/src/SR_analysis/validate/results/illusion_1L_target_vorticity.png differ diff --git a/src/SR_analysis/validate/results/illusion_1L_uncontrolled.png b/src/SR_analysis/validate/results/illusion_1L_uncontrolled.png new file mode 100644 index 0000000..df729c6 Binary files /dev/null and b/src/SR_analysis/validate/results/illusion_1L_uncontrolled.png differ diff --git a/src/SR_analysis/validate/results/illusion_1L_uncontrolled_vorticity.png b/src/SR_analysis/validate/results/illusion_1L_uncontrolled_vorticity.png new file mode 100644 index 0000000..dbfc510 Binary files /dev/null and b/src/SR_analysis/validate/results/illusion_1L_uncontrolled_vorticity.png differ diff --git a/src/SR_analysis/validate/results/illusion_1L_vorticity.png b/src/SR_analysis/validate/results/illusion_1L_vorticity.png new file mode 100644 index 0000000..3f36dae Binary files /dev/null and b/src/SR_analysis/validate/results/illusion_1L_vorticity.png differ diff --git a/src/SR_analysis/validate/run_closed_loop_re400_si.py b/src/SR_analysis/validate/run_closed_loop_re400_si.py new file mode 100644 index 0000000..ac84141 --- /dev/null +++ b/src/SR_analysis/validate/run_closed_loop_re400_si.py @@ -0,0 +1,85 @@ +"""Karman re400 closed-loop validation with configurable sample_interval. + +Tests whether the existing joint deep PySR formula performs better +at shorter control intervals (SI=400, SI=200) than the default SI=800. + +Usage: + conda run -n pycuda_3_10 python validate/run_closed_loop_re400_si.py \\ + --si 400 --device 2 + + conda run -n pycuda_3_10 python validate/run_closed_loop_re400_si.py \\ + --si 200 --device 2 +""" +from __future__ import annotations + +import argparse +import json +import os +import sys + +_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +if _REPO not in sys.path: + sys.path.insert(0, _REPO) +_SRC = os.path.join(_REPO, "src") +if _SRC not in sys.path: + sys.path.insert(0, _SRC) + +from SR_analysis.validate.run_closed_loop import run_validation, load_sindy_coefs +from SR_analysis.validate.predict_pysr import load_pysr_formulas, build_pysr_functions +from SR_analysis.configs import SCENES + + +def main(): + ap = argparse.ArgumentParser(description="Karman re400 short-SI validation") + ap.add_argument("--si", type=int, default=400, help="Sample interval (default: 400)") + ap.add_argument("--device", type=int, default=2, help="GPU device") + ap.add_argument("--steps", type=int, default=0, + help="Steps (0=auto: max(NX/u0/SI, 200))") + args = ap.parse_args() + + scene = "karman_re400" + si = args.si + device = args.device + n_steps = args.steps + + results_dir = os.path.join(os.path.dirname(__file__), "results") + pysr_front = os.path.join(results_dir, "karman_joint_deep_front.json") + pysr_top = os.path.join(results_dir, "karman_joint_deep_top.json") + + # Override sample_interval in scene config + original_si = SCENES[scene]["sample_interval"] + SCENES[scene]["sample_interval"] = si + print(f" Override: sample_interval {original_si} -> {si}") + + # Load PySR formulas + front_expr, top_expr, fn_f, fn_r = load_pysr_formulas(pysr_front, pysr_top) + front_func, top_func = build_pysr_functions(front_expr, top_expr, fn_f, fn_r) + + feat_keys_front = [k for k in fn_f if k != "bias"] + feat_keys_rear = [k for k in fn_r if k != "bias"] + coefs = { + "mode": "pysr", + "front_func": front_func, + "top_func": top_func, + "feat_keys_front": feat_keys_front, + "feat_keys_rear": feat_keys_rear, + "feat_names_front": fn_f, + "feat_names_rear": fn_r, + } + + result = run_validation(scene, coefs, device, n_steps=n_steps, mode="pysr") + + # Save with SI in name + out_dir = os.path.join(os.path.dirname(__file__), "results") + os.makedirs(out_dir, exist_ok=True) + out_path = os.path.join(out_dir, f"karman_re400_pysr_SI{si}.json") + with open(out_path, "w") as f: + json.dump(result, f, indent=2) + print(f"Saved: {out_path}") + + # Restore original config + SCENES[scene]["sample_interval"] = original_si + + +if __name__ == "__main__": + main() diff --git a/src/SR_analysis/validate/run_closed_loop_vortex.py b/src/SR_analysis/validate/run_closed_loop_vortex.py new file mode 100644 index 0000000..55e8f66 --- /dev/null +++ b/src/SR_analysis/validate/run_closed_loop_vortex.py @@ -0,0 +1,340 @@ +"""Closed-loop validator for Vortex cloak scenes. + +Adapted from run_closed_loop.py for vortex geometry: +- No disturbance cylinder (obs[0:12], n_objects=6) +- Action scaling: action*4 + [0,-4,4] +- Transient: n_steps=150, step-aligned similarity (no phase lag) +- Supports --mode pysr (Karman joint formula) and --mode v23 (vortex SINDy) + +Usage: + conda run -n pycuda_3_10 python src/SR_analysis/validate/run_closed_loop_vortex.py \ + --scene vortex_lamb --device 2 --steps 150 --mode pysr \ + --pysr-front validate/results/karman_joint_deep_front.json \ + --pysr-top validate/results/karman_joint_deep_top.json + + conda run -n pycuda_3_10 python src/SR_analysis/validate/run_closed_loop_vortex.py \ + --scene vortex_lamb --device 2 --mode v23 \ + --sindy-results sindy/vortex/sindy_results_v2.json +""" +from __future__ import annotations + +import argparse +import json +import os +import sys +from collections import deque +from typing import Any, Dict, Optional + +import numpy as np + +_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) +if _REPO not in sys.path: + sys.path.insert(0, _REPO) +_SRC = os.path.join(_REPO, "src") +if _SRC not in sys.path: + sys.path.insert(0, _SRC) + +from LegacyCelerisLab import FlowField # noqa: E402 + +from SR_analysis.utils.cfd_interface import ( + load_legacy_configs, compute_similarity, scale_action, +) +from SR_analysis.configs import get_scene, LEGACY_CFG_DIR, FIFO_LEN, CONV_LEN + + +DATA_TYPE = np.float32 +NX = 1280 + + +def run_validation_vortex( + scene_name: str, + device_id: int, + n_steps: int = 150, + sindy_path: Optional[str] = None, + threshold: Optional[float] = None, + mode: str = "pysr", + front_func=None, + top_func=None, + feat_names_front: Optional[list] = None, + feat_names_rear: Optional[list] = None, + out_dir: Optional[str] = None, +) -> dict: + """Run closed-loop validation for a Vortex scene. + + Parameters + ---------- + scene_name : str + Scene name ("vortex_lamb" or "vortex_taylor") + mode : str + "pysr" for PySR formula evaluation, "v23" for SINDy coefficients + front_func, top_func : + PySR callable functions (required for mode="pysr") + feat_names_front, feat_names_rear : + Feature name lists (required for mode="pysr") + """ + cfg = get_scene(scene_name) + u0 = cfg["u0"] + mu = cfg["mu"] + l0 = 20.0 + sample_interval = cfg["sample_interval"] + action_scale = cfg["action_scale"] # 4.0 for vortex + action_bias = cfg["action_bias"] # (0.0, -4.0, 4.0) + n_obj_total = cfg["n_objects_env"] # 6 + vtype = cfg["vortex_type"] + vstrength = cfg["vortex_strength"] + max_steps = cfg.get("max_steps", 150) + + if n_steps <= 0: + n_steps = max_steps + + if out_dir is None: + out_dir = os.path.join(os.path.dirname(__file__), "results") + os.makedirs(out_dir, exist_ok=True) + + if mode == "pysr": + from SR_analysis.validate.predict_pysr import pysr_predict_v23 + elif mode == "v23": + from SR_analysis.validate.run_closed_loop import ( + load_sindy_coefs as _lsc, predict_v23, + ) + if sindy_path is None: + sindy_path = os.path.join( + os.path.dirname(__file__), "..", "sindy", "vortex", "sindy_results_v2.json") + coefs = _lsc(sindy_path, scene_name, threshold=threshold) + else: + raise ValueError(f"Unknown mode: {mode}") + + print(f"\n=== Validating {scene_name} (mode={mode}, device={device_id}, steps={n_steps}) ===") + + # ------------------------------------------------------------------- + # Phase 1: Sensor-only env, record target with vortex + # ------------------------------------------------------------------- + cuda_cfg, field_cfg = load_legacy_configs(LEGACY_CFG_DIR) + field_cfg = field_cfg._replace(viscosity=float(cfg["nu"])) + + ff = FlowField(field_cfg, cuda_cfg, device_id=device_id) + ny = ff.FIELD_SHAPE[1] + + # Add 3 sensors at x=40*L0 + for y_off in [2.0, 0.0, -2.0]: + sc = (40.0 * l0, (ny - 1) / 2 + y_off * l0, 0.0) + ff.add_sensor(sc, l0 / 4.0) + + n_obj_sensors = ff.obs.size // 2 + + # Short stabilize (1*NX/U0) + stabilize_steps_short = int(1 * NX / u0) + print(f" stabilising sensors ({stabilize_steps_short} steps)...") + ff.run(stabilize_steps_short, np.zeros(n_obj_sensors, dtype=DATA_TYPE)) + + # Save clean flow DDF + ff.get_ddf() + ff.save_ddf() + + # Add vortex at x=10*L0 (target position) + ff.add_vortex((10.0 * l0, (ny - 1) / 2, 0.0), + 2.0 * l0, vstrength * u0, 0, vtype) + + # Record target + target_states = np.empty((0, 6), dtype=DATA_TYPE) + for _ in range(max_steps): + ff.run(sample_interval, np.zeros(n_obj_sensors, dtype=DATA_TYPE)) + target_states = np.vstack((target_states, ff.obs.copy())) + print(f" target recorded: {target_states.shape}") + + # ------------------------------------------------------------------- + # Phase 2: Pinball env, norm collection + # ------------------------------------------------------------------- + ff.restore_ddf() + ff.apply_ddf() + + # Add 3 pinball cylinders + ff.add_cylinder((30.0 * l0, (ny - 1) / 2, 0.0), l0 / 2.0) + ff.add_cylinder((31.3 * l0, (ny - 1) / 2 + 0.75 * l0, 0.0), l0 / 2.0) + ff.add_cylinder((31.3 * l0, (ny - 1) / 2 - 0.75 * l0, 0.0), l0 / 2.0) + + n_obj = ff.obs.size // 2 + assert n_obj == 6, f"Expected 6 objects, got {n_obj}" + + # Stabilize with zero action + ff.run(stabilize_steps_short, np.zeros(n_obj, dtype=DATA_TYPE)) + + # Stabilize with bias action + bias_arr = np.zeros(n_obj, dtype=DATA_TYPE) + bias_arr[3] = float(action_bias[0] * u0) + bias_arr[4] = float(action_bias[1] * u0) + bias_arr[5] = float(action_bias[2] * u0) + ff.run(stabilize_steps_short, bias_arr) + + # Add vortex at x=15*L0 (pinball env position) + ff.add_vortex((15.0 * l0, (ny - 1) / 2, 0.0), + 2.0 * l0, vstrength * u0, 0, vtype) + + # Save DDF after vortex (mid-transient reset state) + ff.get_ddf() + ff.save_ddf() + print(" DDF saved with vortex active") + + # Norm collection (zero action) + fifo = deque(maxlen=FIFO_LEN) + for _ in range(FIFO_LEN): + ff.run(sample_interval, np.zeros(n_obj, dtype=DATA_TYPE)) + fifo.append(ff.obs.copy()) + + temp_states = np.array(fifo, dtype=DATA_TYPE) + force_norm_fact = 6.0 * float(np.max(np.abs(temp_states[:, 6:12]))) + sens_deviation = np.mean(temp_states[:, 0:6], axis=0).astype(DATA_TYPE) + sens_norm_fact = np.zeros(6, dtype=DATA_TYPE) + for i in range(6): + sens_norm_fact[i] = 5.0 * float(np.max(np.abs(temp_states[:, i] - sens_deviation[i]))) + + print(f" norm: force_norm_fact={force_norm_fact:.6f}") + + # Bias rollout + ff.apply_ddf() # restore to DDF with vortex active + fifo.clear() + for _ in range(FIFO_LEN): + ff.run(sample_interval, bias_arr) + fifo.append(ff.obs.copy()) + save_states = np.array(list(fifo), dtype=DATA_TYPE) + ff.apply_ddf() + + # Re-fill FIFO from save_states + fifo = deque(maxlen=FIFO_LEN) + for row in save_states: + fifo.append(row) + + # ------------------------------------------------------------------- + # Phase 3: Closed-loop inference + # ------------------------------------------------------------------- + sens_list, actions_list = [], [] + a_prev = np.zeros(3, dtype=np.float64) # normalized action [0,0,0] + a_prev2 = a_prev.copy() + dt_c = sample_interval / 2000.0 # T0 = D/U0 = 2000 steps + + for step in range(n_steps): + obs = fifo[-1] if fifo else np.zeros(12, dtype=np.float32) + obs_prev = list(fifo)[-2] if len(fifo) >= 2 else obs + + # Convert a_prev from normalized to physical omega + a_prev_phys = (a_prev * action_scale + np.array(action_bias, dtype=np.float64)) * u0 + a_prev2_phys = (a_prev2 * action_scale + np.array(action_bias, dtype=np.float64)) * u0 + + if mode == "pysr": + # Predict using PySR formula + feat_keys_front = [k for k in feat_names_front if k != "bias"] + feat_keys_rear = [k for k in feat_names_rear if k != "bias"] + omega_phys = pysr_predict_v23( + obs, a_prev_phys, a_prev2_phys, mu, u0, dt_c, + front_func, top_func, + feat_keys_front, feat_keys_rear, + sensors_raw=np.vstack([obs_prev[0:6], obs[0:6]]), + forces_raw=np.vstack([obs_prev[6:12], obs[6:12]]), + ) + elif mode == "v23": + omega_phys = predict_v23( + obs, a_prev_phys, a_prev2_phys, mu, u0, + coefs["front_coef"], coefs["top_coef"], + coefs["feat_names_front"], coefs["feat_names_rear"], + feat_keys_front=coefs["feat_keys_front"], + feat_keys_rear=coefs["feat_keys_rear"], + ) + else: + raise ValueError(f"Unknown mode: {mode}") + + # Convert physical omega -> normalized action -> legacy array + norm_a = (omega_phys / u0 - np.array(action_bias, dtype=np.float64)) / action_scale + norm_a = np.clip(norm_a, -1.0, 1.0).astype(np.float32) + action_arr = scale_action(norm_a, scale=action_scale, bias=action_bias, + u0=u0, n_total_bodies=n_obj_total) + + ff.run(sample_interval, action_arr) + + obs_new = ff.obs.copy() + fifo.append(obs_new) + sens_list.append(obs_new[0:6]) + actions_list.append(omega_phys.copy()) + + a_prev2 = a_prev.copy() + a_prev = norm_a.copy().astype(np.float64) + + del ff + + # Similarity: step-aligned (no phase lag for transient) + sens_arr = np.array(sens_list, dtype=np.float32) + target_arr = target_states[:n_steps] + n_align = min(sens_arr.shape[0], target_arr.shape[0]) + + if n_align >= CONV_LEN: + sim = compute_similarity(target_arr, sens_arr[:n_align], CONV_LEN) + else: + sim = 0.0 + + action_range = float(np.max(np.abs(actions_list))) + print(f" similarity={sim:.4f} action_range={action_range:.4f}") + + return { + "scene": scene_name, + "mode": mode, + "similarity": sim, + "action_range": action_range, + "n_steps": n_steps, + "threshold": threshold if threshold is not None else "best", + } + + +def main(): + ap = argparse.ArgumentParser(description="Vortex closed-loop validation") + ap.add_argument("--scene", type=str, required=True, + help="Scene name: vortex_lamb or vortex_taylor") + ap.add_argument("--device", type=int, default=2, help="GPU device") + ap.add_argument("--steps", type=int, default=150, + help="Validation steps (default: 150 for vortex)") + ap.add_argument("--mode", type=str, default="pysr", + choices=["pysr", "v23"], + help='Control law mode: "pysr" or "v23"') + ap.add_argument("--sindy-results", type=str, default=None, + help="Path to sindy_results.json (for --mode v23)") + ap.add_argument("--threshold", type=float, default=None, + help="SINDy threshold (for --mode v23)") + ap.add_argument("--pysr-front", type=str, default=None, + help="PySR front JSON (required for --mode pysr)") + ap.add_argument("--pysr-top", type=str, default=None, + help="PySR top JSON (required for --mode pysr)") + ap.add_argument("--out", type=str, default=None) + args = ap.parse_args() + + if args.mode == "pysr": + if args.pysr_front is None or args.pysr_top is None: + raise ValueError("--pysr-front and --pysr-top required for --mode pysr") + from SR_analysis.validate.predict_pysr import ( + load_pysr_formulas, build_pysr_functions, + ) + front_expr, top_expr, fn_f, fn_r = load_pysr_formulas( + args.pysr_front, args.pysr_top) + front_func, top_func = build_pysr_functions( + front_expr, top_expr, fn_f, fn_r) + result = run_validation_vortex( + args.scene, args.device, n_steps=args.steps, mode="pysr", + front_func=front_func, top_func=top_func, + feat_names_front=fn_f, feat_names_rear=fn_r, + out_dir=args.out, + ) + else: + result = run_validation_vortex( + args.scene, args.device, n_steps=args.steps, mode="v23", + sindy_path=args.sindy_results, threshold=args.threshold, + out_dir=args.out, + ) + + out_name = f"{args.scene}_{args.mode}" + out_dir = args.out or os.path.join(os.path.dirname(__file__), "results") + os.makedirs(out_dir, exist_ok=True) + out_path = os.path.join(out_dir, f"{out_name}.json") + with open(out_path, "w") as f: + json.dump(result, f, indent=2) + print(f"Saved: {out_path}") + + +if __name__ == "__main__": + main() diff --git a/src/analysis_knowledge.md b/src/analysis_knowledge.md new file mode 100644 index 0000000..b1240cf --- /dev/null +++ b/src/analysis_knowledge.md @@ -0,0 +1,357 @@ +# 主分析知识库 + +## 文档作用 + +这份文档只负责保存当前主分析线里已经形成的**长期判断、边界条件、方法论经验和不该忘的坑**。 + +它不安排执行顺序,也不写“下一步做什么”。凡是优先级、阶段、第一轮交付物,统一写在 `analysis_notes`。 + +这里重点保留: + +- 这一轮调研后已经比较稳的物理判断 +- 哪些文献对当前项目真的有用,分别用在什么层级 +- 哪些解释现在可以说,哪些不要说过头 +- 当前网格分辨率下,哪些分析是可信的,哪些不可信 + +## 一、当前最重要的认识变化 + +这一轮最大的变化,不是又多知道了几篇文献,而是**分析层级变了**。 + +之前最容易滑向两个方向: + +- 要么把机制写成 `obs -> act -> force -> signature` +- 要么试图直接搬壁面涡量理论,做单圆柱边界层级别的解释 + +现在更稳的理解是: + +\[ +\text{obs} \rightarrow \text{act} \rightarrow \text{near-body correction} \rightarrow \text{wake structure} \rightarrow \text{force / future signature} +\] + +也就是说: + +- **force 依然重要,但更像近体区修正过程的低维投影,而不是唯一第一性机制** [Zhu15]。 +- **Chen Tao 线依然重要,但它提供的主要是因果方向,不是当前网格下可直接照搬的壁面分析模板** [Che19, Che21b, Ter21]。 +- **当前最可信、也最值得主打的层级,是 pinball 整体尺度的 correction field**,而不是单圆柱 wall-scale 细节。 + +## 二、Chen Tao 这条线对本项目到底有什么用 + +### 1. 有用的不是“旋转会改变表面量”这句常识 + +真正有用的是下面这条因果链: + +\[ +\text{rotation} \rightarrow \text{surface condition change} \rightarrow \text{near-body vorticity organization} \rightarrow \text{shear-layer bias} \rightarrow \text{wake / force / signature} +\] + +这条线的价值在于,它逼着解释停留在“近体区结构如何被改掉”这一层,而不是直接从动作跳到力,或者从动作跳到远场 wake。 + +### 2. 最适合当前项目的 Chen–Liu 文献分工 + +| 论文 | 在当前项目里的角色 | +|---|---| +| [Che19] | 最重要的桥梁。告诉我们表面压力、skin friction、boundary enstrophy flux 是耦合的,因此近体区源项不是空话。| +| [Che21b] | 提醒我们这些源项不是静态量,而有时空演化,并且和 separation / attachment 及近壁相干结构相关。| +| [Che24d] | 把 boundary vorticity flux 继续拆成 orbital rotation 与 spin,适合用来升级机理语言,但不是第一轮主证据。| +| [Che24e] | 提醒 Q-criterion 类量不要解释过满,边界上的 strain / enstrophy / Q 源项是不同层次。| +| [Che26] | 这是这一支到 JFM 的理论总论,更像上层统一框架,而不是 rotating-cylinder wake 的直接分析手册。| + +### 3. 当前项目里最该保留的 Chen 线结论 + +- 旋转不是直接“指定一个目标 wake”,而是先改写近体区源项和剪切层出生条件。 +- 受力不是与涡量竞争的替代机制,而更像近体区涡量组织变化后的整体积分响应。 +- 若网格不支持壁面精细诊断,仍然可以保留“近体区源项先变、远场 observable 后变”这条因果顺序。 + +## 三、哪些部分现在不要硬做 + +当前 CFD 中单圆柱只占十多格,这是一个非常硬的边界条件。由此带来几个必须长期保留的判断: + +| 不应硬做的分析 | 原因 | +|---|---| +| 单圆柱边界层厚度与细节 | 分辨率不够 | +| 单圆柱精确分离点、附着点 | 数值可信度不足 | +| 壁面涡量通量逐点定量值 | 网格不足以支撑 | +| 以 wall-scale 量做主机制证据 | 容易把分辨率不支持的内容写过满 | + +因此,对当前项目最稳的表述是: + +- 可以引用 Chen–Liu 线来支持“壁面/近体区过程在前”; +- 但自己的主证据必须落在 **pinball-scale near-body correction** 上。 + +## 四、为什么三场分解是当前最关键的方法论 + +这是这一轮最重要的方法收获。 + +如果直接看控制场,很容易把三种影响混在一起: + +- 入射来流本身的结构 +- pinball 作为障碍物的阻挡作用 +- 旋转控制额外施加的修正 + +因此必须至少分成三类场: + +- 入射参考场 \(q_{in}\) +- 固定 pinball 场 \(q_{blk}\) +- 控制 pinball 场 \(q_{ctl}\) + +然后定义两个差分场: + +\[ +\Delta q_{blk} = q_{blk} - q_{in} +\] + +\[ +\Delta q_{ctl} = q_{ctl} - q_{blk} +\] + +这里最关键的不是公式本身,而是它们对应的解释权。 + +- \(\Delta q_{blk}\) 代表 **几何阻挡带来的被动破坏**。 +- \(\Delta q_{ctl}\) 代表 **旋转控制额外施加的主动修正**。 + +一旦这两个量分开,很多原本混乱的问题会立刻清楚: + +- cloak 不是“什么都不产生”,而是主动修正去抵消被动破坏; +- illusion 不是“pinball 变成了目标物体”,而是在自身基线 wake 上叠加 target-like correction。 + +## 新增补充:wake-to-force 这条线为什么重要 + +这一轮新增的第二条有效文献线,不是 another force formula,而是 **wake-to-force**。它真正补上的,是 `near-body correction -> force -> signature` 这一段的物理层次。[Noc99, Una97, Kan17b, Geh23, Gom16] + +这条线最值得保留的结论不是“用 wake 可以算出力”,而是以下三条: + +### 1. 力与 wake 的关系分层成立 + +较早的 control-volume / momentum 公式说明,只要控制体选得合理,近尾迹速度场本身就足以恢复瞬时力的主变化。[Una97, Noc99] 这至少证明: + +- force 不是脱离流场的黑箱量; +- 近尾迹与力之间确实存在可重复、可计算的映射。 + +### 2. 真正最该关注的是 body-connected wake structure + +minimum-domain impulse 理论的关键判断是:对瞬时力真正直接起作用的,不必是整个成熟 far wake,而是**仍与物体动态相连的 vortical zone**;已经 detached 的紧致尾涡,其净力贡献可以视为零。[Kan17b] + +这对当前 pinball 的帮助极大,因为它把“目标尾迹要几个 \(D\) 才形成”和“力的主决定层更靠前”区分开了。更稳的口径应当是: + +- signature 的形成需要发展距离; +- force-relevant mechanism 更集中在 near-body / body-connected 区域。 + +### 3. 同样的涡量,不同位置,对 force 的投影不同 + +vortex force decomposition 进一步说明,力不是简单由“总涡量大小”决定,而取决于涡结构的位置、与 body-induced velocity 的耦合方式,以及它是 body-generated、shed 还是 external。[Geh23] + +对 pinball,最可迁移的不是照抄公式,而是保留这个判断: + +- 同一强度的局部涡量结构,若位置和耦合方式不同,对总 force 的贡献也会不同; +- 因此后续完全可以谈 **force-relevant structures**,而不是只谈 energetic structures。 + +## 新增补充:这条线如何改写当前主链 + +wake-to-force 与 Chen 线结合后,当前最稳的主链不应再写成 + +\[ +\text{obs} \rightarrow \text{act} \rightarrow \text{force} \rightarrow \text{signature} +\] + +也不应写成过于粗糙的 + +\[ +\text{obs} \rightarrow \text{act} \rightarrow \text{wake} +\] + +而应写成: + +\[ +\text{obs} \rightarrow \text{act} \rightarrow \text{near-body correction} \rightarrow \text{body-connected wake structure} \rightarrow \text{force} \rightarrow \text{future signature} +\] + +其中: + +- Chen 线负责说明为什么 `act` 的第一作用层是近体区源项与涡量组织,而不是远场结果。[Che19, Che21b, Ter21] +- wake-to-force 线负责说明为什么 force 最好理解为 body-connected near-wake organization 的低维投影,而不是与涡量竞争的另一套机制。[Kan17b, Geh23, Gom16] +- OID/CCD 线负责把这条通道压缩成少数结构坐标,并区分 force-relevant 与 signature-relevant structures。[Sch12, Lyu23] + +## 五、对 cloak 与 illusion 的当前最稳表述 + +### cloak + +现在最稳的写法是: + +- fixed pinball 会在任务相关区域内产生一个 blockage wake; +- DRL 旋转产生的 correction field 会尽量抵消这部分 wake; +- 合力接近 0 不是全部机制,而是这种 wake cancellation 的积分表现。 + +因此,如果分析成立,应看到: + +\[ +\Delta q_{ctl} \approx -\Delta q_{blk} +\] + +至少在任务相关区域里呈现这种趋势。 + +### illusion + +现在最稳的写法是: + +- fixed pinball 先生成自身基线 wake; +- 控制不是把 wake 全部“消掉”,而是做一个 target-oriented correction; +- force matching 之所以有效,是因为它约束了整体节律与积分响应,但真正被操控的是 correction field。 + +如果分析成立,应看到: + +\[ +\Delta q_{ctl} +\] + +接近“目标物体相对 fixed pinball 的残差结构”。 + +## 六、force 在当前项目中的正确位置 + +这是最容易说错的地方之一。 + +### 现在可以说的 + +- force 是极重要的 observable。 +- force 对 DRL reward 有效,说明它确实和任务结构高度相关。 +- force 很可能是近体区修正场的低维、强相关投影,因此特别适合作为控制反馈量。 +- 更具体地说,当前最稳的理解不是“force 来自整个远场尾迹”,而是:force 更像 **body-connected near-wake organization** 的积分响应或低维投影。[Kan17b, Geh23, Gom16] + +### 现在不要说得太满的 + +- “控制机制本质上就是 force tracking” +- “只要合力为 0,就自然 cloak” +- “illusion 的本质就是匹配目标力” + +更稳的说法是: + +- force 是 near-body correction 的一个重要投影; +- 它之所以有用,是因为它和 task-relevant structures 强相关; +- 但真正被操纵的,仍然更像是 correction field 而不是一个单独积分量。 + +## 七、OID / CCD / PCD 在当前主线中的正确位置 + +这条线现在不应再被理解成“对原始全流场做更高级模态分解”。 + +更稳的理解是: + +- 原始全流场里混着入射结构、阻挡结构和控制结构; +- 真正需要找的是 **control-induced correction structures**; +- 因此 OID / CCD / PCD 最好作用在 \(\Delta q_{ctl}\) 或其低维系数上。 + +### 当前最有价值的任务 + +| 方法 | 现在最值得做的事 | +|---|---| +| correction-field POD | 看控制修正主要落在哪几个低维方向上 | +| force-OID | 找最影响 force 的 correction structures | +| signature-OID | 找最影响 future signature 的 correction structures | +| PCD / CCD | 在显式时延下区分 source / descendant correction modes [Lyu23] | + +这里需要新增一条长期保留的解释纪律:**force-OID** 与 **signature-OID** 不应先验合并。更稳的工作假设是: + +- controller 首先调制一小组 force-relevant near-wake structures; +- 这些结构经过若干个 \(D\) 的对流与相互作用后,才演化为 signature-relevant downstream structures。[Kan17b, Sch12, Lyu23] + +因此,如果后续真的看到 force-OID 与 signature-OID 差异明显,这首先应被视为潜在机制结果,而不是算法失败。只有在 POD rank、时延、标准化和数据量都排查完后,才适合讨论该差异是否可信。 + +### 这条线最关键的判断 + +若少数 correction modes 就能: + +- 解释大部分 force 变化 +- 预测 future signature 的改善 +- 在 cloak 与 illusion 间提供可比较结构 + +则说明“控制调用的是少数 pinball-scale correction structures”这一主张成立。 + +## 八、SR / SINDy 的正确角色 + +这条线现在最容易被高估,也最容易被低估。 + +### 它不该单独承担的事 + +- 不应让 `obs -> act` 一条线独自承担全部物理解释。 +- 不应把高拟合度自动当成“机制已明”。 + +### 它现在真正该承担的事 + +- 作为 `obs -> act` 的白箱接口; +- 给出控制律依赖哪些低维量的证据; +- 最终和 correction field 及 OID/CCD 拼起来,形成闭环解释。 + +因此最合理的三段拼法是: + +\[ +\text{SR/SINDy}: \text{obs} \rightarrow \text{act} +\] + +\[ +\text{difference-field analysis}: \text{act} \rightarrow \Delta q_{ctl} +\] + +\[ +\text{wake-to-force / Chen}: \Delta q_{ctl} \rightarrow \text{body-connected wake structure} \rightarrow \text{force} +\] + +\[ +\text{OID/CCD}: \text{force-related structure} \rightarrow \text{future signature} +\] + +这也意味着,当前阶段不应要求 SINDy/SR 一条线独自把全部物理机制说完。更稳的节奏是:先让 SINDy/SR 找到共享骨架与可闭环公式,再等 CCD/OID 与 wake-to-force 这边把结构中介对象做扎实之后,回头精简和改写白箱公式。 + +## 九、这一轮最值得长期保留的方法论经验 + +### 经验 1 + +**不要直接在原始控制场上谈“机制”。** + +先拆出: + +- 来流本身是什么 +- 几何阻挡本身做了什么 +- 控制额外改了什么 + +否则很容易把“本来就有的结构”误当成“控制生成的结构”。 + +### 经验 2 + +**当前问题的自然尺度不是单圆柱 wall-scale,而是 pinball-scale correction-scale。** + +这意味着: + +- 可以放弃一些不可信的细节分析; +- 但反而更容易得到跨场景稳定的主结论。 + +### 经验 3 + +**Chen 线最该保留的是因果顺序,不是细节模板。** + +它告诉我们: + +- 近体区在前,远场 observable 在后; +- 受力是近体区组织变化后的结果; +- 这就足以为 correction field 主线提供物理正当性。 + +### 经验 4 + +**Lyu / Schlegel 这条线最适合用来识别“控制额外改出来的任务相关结构”。** + +不是所有 energetic 结构都重要; +真正重要的是 observable-relevant correction structures。[Sch12, Lyu23] + +## 十、当前最值得长期保留的两句话 + +\[ +\boxed{\text{当前项目最有价值的主张,不是“DRL 学会了某个力学量”,而是“DRL 学会了对 pinball 近体区修正场的低维调制”。}} +\] + +同时必须保留另一句: + +\[ +\boxed{\text{Chen Tao 线提供的是正确的因果方向,而不是当前网格下可直接照搬的壁面分析模板。}} +\] + +这两句话一起保留,最能防止后续再次滑回两个极端: + +- 把全部机制压扁成 force tracking; +- 把分辨率并不支持的壁面细节写得过满。 diff --git a/src/analysis_notes.md b/src/analysis_notes.md new file mode 100644 index 0000000..040d1ca --- /dev/null +++ b/src/analysis_notes.md @@ -0,0 +1,1220 @@ +## 课题背景与当前目标 + +当前工作的核心不是再证明 pinball 可以被控制,而是解释 **机器学习控制究竟通过哪些流动结构通道改写尾迹**。项目对象是 2D 槽道中的 fluidic pinball:三个等径圆柱按等边三角形排布、各自可独立旋转;控制器利用 **各圆柱受力** 与 **下游三点速度观测** 进行闭环控制,在典型的二维脱涡雷诺数范围内实现 hydrodynamic stealth、illusion 和 wake erasure。已有数值结果已经证明这几类目标可以实现,且部分 stealth 与 illusion case 已完成实验验证。 + +当前真正的瓶颈不是“控制是否有效”,而是如何把以下链条写清楚:控制器看到了什么,动作首先改变了哪些受力或近尾迹结构,这些结构又怎样传播到下游并决定最终的 signature。换言之,文章和后处理最缺的是 + +\[ +\text{obs} \rightarrow \text{act} \rightarrow \text{flow structure} \rightarrow \text{signature} +\] + +而不仅是 `obs -> act` 或“控制前后流场长得不一样”。因此,这份任务书的目标是把已有能力组织成一条可重复的后处理链:从 `obs`、`act`、force、全流场和局部体力 probing 出发,解释控制如何改变低维结构、均值尾迹和下游 signature。对圆柱尾迹与受控 wake,最稳的分析框架是把 **控制代价**、**mean–fluctuation 重组**、**observable-related structure** 和 **localized forcing 诊断** 接起来,而不是单看 DTW 或单看全场误差 [Ber06, Tad10, Man15, Sch12, Lyu23, Jin21]。 + +## 新人首先需要知道的系统设定 + +### 研究对象 + +| 项目 | 内容 | 备注 | +|---|---|---| +| 几何对象 | fluidic pinball | 三个等径圆柱,前一后二,中心构成等边三角形 | +| 流动类型 | 2D 槽道不可压缩流 | 当前主线为抛物面来流,也有上游扰动来流工况 | +| 典型雷诺数 | 约 `Re_D ~ O(10^2)` | 目前分析重点可按 `Re≈100` 附近理解,但文稿中必须始终写清具体口径 | +| 执行器 | 三圆柱独立旋转 | 每个圆柱一个控制输入 | +| 观测 | 圆柱受力 + 下游三点速度 | 三个传感器各含 `u,v` 两分量 | +| 控制目标 | stealth / illusion / erasure | 本质上都是 downstream wake signature shaping | +| 评价方式 | 目标时序匹配 + 力学量辅助 | 当前主指标是 DTW,也保留 force / spectrum / field error | + +### 为什么选 pinball + +pinball 不是随意挑的玩具系统,而是一个兼具复杂性和可分析性的规范化平台。它同时具有: + +- 多输入多输出闭环控制结构 +- 近尾迹强非线性与多时间尺度 +- 可清楚分辨的几何分工:前圆柱、上下后圆柱 +- 已有较成熟的低维建模与模态分析背景 [Den18b, Den20, Den21, Li22b] + +因此它既足够复杂,能承载机器学习控制;又足够“干净”,适合追问控制到底在调什么结构。 + +### 当前课题和传统主动流控的区别 + +当前目标不是传统 AFC 中常见的单一 drag reduction 或 force tracking,而是 **让下游观测到的时空 signature 变成指定目标**。因此,真正的控制对象不是某个积分量,而是下游可感知流场。对当前项目,更准确的说法是: + +- stealth:让 pinball 的 downstream signature 接近“没有 pinball”或目标背景流 +- illusion:让 pinball 的 downstream signature 接近另一个目标物体或目标流场 +- erasure:强调抹去原有 wake signature,可视为 stealth 的一个极端形式 + +这也是为什么单纯按全场能量排序的 POD 不够,后续必须围绕 observable 去定义低维结构坐标。 + +### 当前控制与学习路线 + +| 模块 | 当前状态 | 用途 | +|---|---|---| +| DRL 闭环控制 | 已完成多类 case | 证明 stealth / illusion / erasure 可实现 | +| DANTE 树搜索 | 已用于替代或补充 DRL 探索 | 加速寻找有效策略 | +| SINDy / SR | 已用于分析 `act-obs` 关系 | 提升策略可解释性 | +| 实验验证 | 部分 stealth 与 illusion 已完成 | 支撑数值结果不是纯仿真特例 | +| 后处理分析 | 正在补强 | 目标是把控制与结构机制连起来 | + +当前最缺的不是再给出一个更高分的控制器,而是把黑箱策略推进成: + +\[ +\text{obs} \rightarrow z \rightarrow \text{act} +\] + +并进一步说明这个低维状态 \(z\) 对应哪些 flow structures、force channels 和 downstream signatures。 + +### 这份任务书要回答的核心问题 + +后续所有分析,最终都服务于下面几个问题: + +1. 控制器到底在调哪些流动结构,而不是仅仅输出了什么动作 +2. 这些结构首先体现在哪些量上:受力、均值尾迹、剪切层、回流区还是主模态 +3. stealth 与 illusion 是在利用同一条结构通道,还是两类不同机制 +4. 简化后的白箱控制律是否作用于少数低维结构态,而不是直接“记住”高维流场 +5. CCD / OID / 相关分解是否能比普通 POD 更准确地识别这些与任务相关的结构 + +## 这一轮调研后新增的关键判断 + +这一轮最重要的变化,不是又多知道了几篇壁面涡量论文,而是**分析层级要下调到当前网格真正能承受的尺度**。 + +虽然 Chen Tao 这条线给出了很好的因果方向:旋转先改写壁面/近壁源项,再改写剪切层、受力和尾迹 [Che19, Che21b, Ter21, Zhu15],但当前 pinball CFD 里单圆柱只占十多格,单圆柱边界层、精确分离点、壁面涡量通量点值都不可信。因此,这条线对本项目最重要的用途不是照着做 wall-resolved diagnosis,而是帮助把机制顺序写对: + +\[ +\text{rotation} \rightarrow \text{near-body correction} \rightarrow \text{wake structure} \rightarrow \text{force / signature} +\] + +这意味着对当前项目,更稳的表述应当是: + +- `force` 仍然很重要,但更像近体区修正过程的低维、强相关投影,而不是唯一第一性原因 +- 当前最可信的主分析对象不是单圆柱壁面量,而是 **pinball 整体尺度的 near-body correction field** +- OID / CCD / PCD 最好优先作用在“控制额外改出来的那部分结构”上,而不是直接拿原始全流场做全部解释 + +这并不否定原来 `obs -> z -> act -> force/power -> wake structure -> signature` 的主链,而是要求在 `act` 与 `force/wake` 之间再补上一层更贴近当前数值分辨率的中介: + +\[ +\text{obs} \rightarrow \text{act} \rightarrow \text{near-body correction} \rightarrow \text{force/wake} \rightarrow \text{signature} +\] + +后面几项任务里的新增内容,基本都围绕这一层展开。 +## 为什么后处理主线必须引入 observable-related decomposition + +当前项目的 reward 和成功判据,本质上都围绕传感器时序和目标 signature 展开。也就是说,真正需要解释的是 **哪些流场结构最影响 observable**,而不是哪些结构能量最大。POD 提供统一坐标系,但它按能量排序;CCD 则更适合回答“与某个 observable 最相关的结构是什么”,并且可以显式纳入时间延迟 [Lyu23]。这正好契合当前项目中最关键但最难的一步:把当前流场结构与未来下游 signature 联系起来。 + +因此,后续最值得建立的主链条不是停在 `obs -> act`,而是 + +\[ +\text{obs} \rightarrow z \rightarrow \text{act} \rightarrow \text{force/power} \rightarrow \text{wake structure} \rightarrow \text{signature} +\] + +其中 \(z\) 不是抽象潜变量,而是由公共 POD、observable-related coordinates、回流区几何量和必要的频域量共同构成的低维状态 [Sch12, Lyu23, Li22b]。 + +当前数值能力足以支撑一套分层分析:实时获得三个圆柱的独立转速与受力、下游一排三个传感器的速度时序、完整 2D 流场快照,以及在一个或多个小圆形区域内施加局部体力。这意味着可以把主线明确写成 + +\[ +\text{obs} \rightarrow z \rightarrow \text{act} \rightarrow \text{force/power} \rightarrow \text{wake structure} \rightarrow \text{signature} +\] + +其中 \(z\) 不是抽象潜变量,而是由公共 POD、observable-related coordinates、回流区几何量和必要的频域量共同构成的低维状态 [Sch12, Li22b, Tad10]。 + +## 系统设定与统一记号 + +采用以下统一记号,后续所有脚本、文件名和数据结构都按这一套执行。 + +| 量 | 记号 | 说明 | +|---|---|---| +| 采样索引 | `n=0,1,2,...` | 每隔若干 CFD 步采一次样,同时更新一次控制 | +| 物理时间 | \(t_n\) | 与采样点一一对应 | +| 三圆柱转速 | \(\Omega_i(t_n)\), `i=1,2,3` | 控制输入 | +| 三圆柱受力与力矩 | \(F_{x,i},F_{y,i},M_{z,i}\) | 实时可得 | +| 三个传感器速度 | \(s(t_n)\in\mathbb{R}^6\) | 每个传感器含 \(u,v\) 两分量 | +| 目标传感器时序 | \(s_{tar}(t_n)\) | 用于 DTW 与相似度 | +| 流场 | \(u(x,y,t_n),v(x,y,t_n)\) | 全域 2D 快照 | +| 局部体力 | \(\mathbf f_p(x,y,t)\) | 只允许作用在一个或多个圆形区域 | + +雷诺数口径也必须固定写法: + +- \(Re_D\):以 pinball 单个圆柱直径为特征长度 +- \(Re\):以两倍直径的标准扰流圆柱尺度为特征长度 + +所有图题、case 标签、数据目录和表格都必须显式写明使用哪一个口径,不能混写。 + +## 数据输出与目录结构 + +为了避免后期反复重跑,建议所有 case 都一次性导出下列数据。 + +| 数据组 | 变量 | 最低格式 | +|---|---|---| +| 流场 | `U[n, iy, ix]`, `V[n, iy, ix]` | float32 或 float64 | +| 压力可选 | `P[n, iy, ix]` | 若存储允许则保留 | +| 力与力矩 | `Fx[n,3]`, `Fy[n,3]`, `Mz[n,3]` | 每个圆柱一列 | +| 控制量 | `Omega[n,3]` | 与受力严格同步 | +| 观测量 | `Obs[n,6]`, `Obs_tar[n,6]` | 顺序固定 | +| case 元数据 | `meta.json` | Re 口径、来流类型、目标类型、采样步长 | +| probing 元数据 | `probe.json` | 圆区中心、半径、方向、频率、幅值、起止时间 | + +建议统一目录: + +- `case_xxx/raw/`:原始同步数据 +- `case_xxx/derived/`:均值场、RMS、涡量、回流区、谱、POD 系数等中间量 +- `case_xxx/fig/`:标准图 +- `case_xxx/report/`:统计表与摘要 + +## 时间对齐与窗口原则 + +控制效应不会立刻在传感器上体现,而是存在明显对流时延。当前设定里,控制通常需要大约 \(L_x/U\) 的传播时间才能在下游传感器上稳定体现;同时每个脱涡周期只有约 10–20 个采样点。因此所有分析都必须把 **时延** 和 **窗口长度** 当成一级问题,而不是事后修正 [Jin21, Jin19b]。 + +定义采样间隔为 \(\Delta t_s\),主脱涡周期为 \(T_{shed}\),则每周期样本数约为 + +\[ +N_T = \frac{T_{shed}}{\Delta t_s} \approx 10\text{--}20 +\] + +定义控制到传感器的对流时延近似为 + +\[ +\tau_c \approx \frac{L_x}{U_{conv}} +\] + +实际代码中不应只用固定 \(U_{in}\),而应在 pinball 下游到传感器之间取一个代表性的局部平均对流速度 \(U_{conv}\)。随后定义对应的采样滞后步数 + +\[ +N_c = \mathrm{round}\left(\frac{\tau_c}{\Delta t_s}\right) +\] + +所有 `act -> obs`、`force -> obs`、probing 响应和窗口化 DTW 计算,默认都要至少测试三种滞后: + +- `0` +- `N_c` +- `N_c \pm 0.25 N_T` + +这样可以判断最佳对齐是否真接近物理对流时延。 + +## 任务 1 + +## 基础后处理 + +第一批结果必须先把均值、脉动、功率和回流区做扎实。这一层风险最低,但几乎一定能给出物理信息。 + +### 1.1 均值场与脉动场 + +对每个 case,在去掉初始过渡后,计算时间均值场 + +\[ +\bar u(x,y)=\frac{1}{N}\sum_{n=1}^{N} u(x,y,t_n),\qquad +\bar v(x,y)=\frac{1}{N}\sum_{n=1}^{N} v(x,y,t_n) +\] + +定义脉动场 + +\[ +u'(x,y,t_n)=u(x,y,t_n)-\bar u(x,y),\qquad +v'(x,y,t_n)=v(x,y,t_n)-\bar v(x,y) +\] + +以及 RMS 图 + +\[ +\mathrm{RMS}_u(x,y)=\sqrt{\frac{1}{N}\sum_{n=1}^{N} u'(x,y,t_n)^2} +\] + +\[ +\mathrm{RMS}_v(x,y)=\sqrt{\frac{1}{N}\sum_{n=1}^{N} v'(x,y,t_n)^2} +\] + +同时建议输出均值涡量与瞬时涡量: + +\[ +\omega(x,y,t)=\frac{\partial v}{\partial x}-\frac{\partial u}{\partial y} +\] + +### 1.2 回流区几何量 + +由均值流定义回流区边界为 \(\bar u=0\) 等值线。提取: + +- 回流区长度 \(L_r\) +- 回流区面积 \(A_r\) +- 上下半区面积差 +- 尾迹中心线偏移 + +圆柱尾迹的受控变化往往先表现为均值流重构,再改变脉动的增长与饱和,因此这些量应视为主指标,而不是附属图 [Tad10, Man15, Kha24c]。 + +### 1.3 力、力矩与控制功率 + +对每个圆柱定义瞬时控制功率 + +\[ +P_{c,i}(t_n)=M_{z,i}(t_n)\,\Omega_i(t_n) +\] + +总控制功率 + +\[ +P_c(t_n)=\sum_{i=1}^{3} P_{c,i}(t_n) +\] + +对每个 case 输出: + +- \(P_{c,i}(t)\) 与 \(P_c(t)\) 时间序列 +- 平均功率 \(\overline{P_c}\) +- 功率 RMS +- 功率谱 +- 三个圆柱功率占比 + +旋转控制的功率核算必须和气动收益分开记账,否则很容易得到“结果有效但代价模糊”的控制解释 [Ber06]。 + +### 1.4 传感器误差与相似度 + +定义瞬时传感器误差向量 + +\[ +e_s(t_n)=s(t_n)-s_{tar}(t_n) +\] + +定义点态误差范数 + +\[ +E_s(t_n)=\lVert e_s(t_n)\rVert_2 +\] + +DTW 仍保留为最终任务指标,但建议补两个更轻量的窗口量: + +- 窗口化欧氏误差均值 +- 窗口化相关系数或相位滞后 + +因为 DTW 适合最终评分,不适合作为所有中间分析唯一指标。 + +### 1.5 新增:三场分解与 correction field + +这是这一轮调研后最值得优先加入的分析层。目的不是替代前面的基础后处理,而是把“pinball 自身阻挡的影响”和“旋转控制额外施加的影响”分开。 + +对每个主分析工况,至少准备三类同步场: + +| 记号 | 含义 | +|---|---| +| \(q_{in}(x,t)\) | 入射参考场:没有 pinball,只有背景来流或上游扰动 | +| \(q_{blk}(x,t)\) | 固定 pinball 场:pinball 在场内,但不旋转 | +| \(q_{ctl}(x,t)\) | 控制 pinball 场:pinball 按 DRL 策略旋转 | + +这里的 \(q\) 可以是速度场、涡量场、压力场,或其低维投影。 + +随后定义两个差分场: + +\[ +\Delta q_{blk} = q_{blk} - q_{in} +\] + +表示 **pinball 几何阻挡本身** 对流场造成的扰动;再定义: + +\[ +\Delta q_{ctl} = q_{ctl} - q_{blk} +\] + +表示 **在 pinball 已经存在的前提下,旋转控制额外施加的修正**。 + +这一步对 cloak 与 illusion 都特别关键。 + +- 对 cloak,更合理的任务表述不是“合力为零”,而是:控制产生的 correction field 尽量抵消 blockage wake,因此在任务相关区域里倾向于看到 + +\[ +\Delta q_{ctl} \approx -\Delta q_{blk} +\] + +- 对 illusion,更合理的任务表述不是“pinball 直接变成目标物体”,而是:控制在 fixed-pinball 的基线 wake 上叠加一个 target-like correction。若目标物体在相同来流下的场记为 \(q_{tar}\),则理想上应比较 + +\[ +\Delta q_{ctl} +\] + +与 + +\[ +q_{tar} - q_{blk} +\] + +之间的接近程度。 + +第一轮不必追求精细壁面量,只需先在 pinball 整体尺度上回答: + +- pinball 本来会把流场改成什么样 +- 控制又额外把它改成什么样 +- 这部分额外修正是否具有稳定结构,并继续传到 force 与 signature + +推荐至少分三个区域看这些差分场: + +- pinball 紧邻区 +- 近尾迹形成区 +- 下游传感器区 + +并优先用粗粒化指标,而不是单点壁面量。例如: + +- 正负涡量面积或积分 +- 上下半区涡量不平衡 +- 回流区长度与面积变化 +- 近尾迹中心线偏移 +- 分区能量 / enstrophy 差分 + +这一步若能跑通,后面的 OID、CCD 和 SR 才有一个真正清楚的中介对象可接。 + +### 1.6 新增:wake-to-force 视角下的 near-wake 层级 + +这一轮需要补上的,不只是“控制额外改了什么流场”,还包括:**这些改动里哪些部分最直接投影到 force,哪些部分再继续发展成 downstream signature**。wake-to-force 文献给出的最重要启发,不是又多了一套算力公式,而是指出应把“与物体仍动态相连的近尾迹结构”与“已经成熟脱落的 far wake”分开看。[Noc99, Kan17b, Geh23] + +这会把主链条进一步细化为: + +\[ +\text{obs} \rightarrow \text{act} \rightarrow \Delta q_{ctl} \rightarrow \text{body-connected wake structure} \rightarrow \text{force} \rightarrow \text{future signature} +\] + +对当前 pinball,这条补充很重要,因为它正好化解一个表面矛盾: + +- **目标 signature** 的形成确实需要若干个 \(D\) 的对流、融合和发展; +- 但**瞬时力与控制直接作用的主结构层**未必需要延伸到整个成熟 far wake,而更可能集中在 pinball 后方有限的 body-connected 区域。[Kan17b] + +因此,第一轮分析时不宜把“离 pinball 很近的结构不可能像 target”误写成“近尾迹对机制不重要”。更稳的表述应当是: + +- 近尾迹负责 force-relevant structure; +- 更下游的发展区负责 signature formation; +- 两者应区分但不能割裂。[Kan17b, Sch12, Lyu23] + +在执行层面上,这意味着至少应把后处理区域分成三层: + +| 区域 | 主要问题 | 更适合接哪条线 | +|---|---|---| +| pinball 紧邻区 | 旋转先改了什么近体区修正 | Chen 线 / correction field [Che19] | +| body-connected near wake | 哪些结构最先投影到 force | wake-to-force / force-OID / force-CCD [Noc99, Kan17b, Geh23, Sch12] | +| downstream sensor zone | 哪些结构最终决定 future signature | signature-OID / CCD / delay-aware 相关分解 [Sch12, Lyu23] | + +## 任务 2 + +## 公共 POD 与低维状态 + +POD 的用途不是直接给最终机制下结论,而是建立统一坐标系,让不同 case 的结构变化可比较 [Den18b, Den20, Tai17]。 + +### 2.1 公共 POD + +把 uncontrolled、stealth、illusion、target,以及必要的 disturbed inflow case 的快照拼在一起,构造状态向量 + +\[ +q(t_n)= +\begin{bmatrix} + u(x_1,y_1,t_n),\ldots,u(x_M,y_M,t_n), + v(x_1,y_1,t_n),\ldots,v(x_M,y_M,t_n) +\end{bmatrix}^{\top} +\] + +对去均值后的矩阵做 SVD: + +\[ +Q = U \Sigma V^{\top} +\] + +得到空间模态 \(\phi_k\) 与时间系数 \(a_k(t_n)\)。 + +### 2.2 必须输出的 POD 结果 + +| 输出 | 说明 | 用途 | +|---|---|---| +| 前 6 阶模态 | 空间结构图 | 看主导 shedding pair、shift-like mode | +| 模态能量占比 | 条形图 | 看各 case 能量重分配 | +| 主系数时间序列 | \(a_1,a_2,a_3,a_4\) | 看周期、相位和稳定态 | +| 主系数相图 | 如 \((a_1,a_2)\) | 看 attractor 与状态迁移 | + +### 2.3 主系数与观测/动作/力的关联 + +至少计算下列相关量: + +\[ +\mathrm{corr}(a_k,\Omega_i),\qquad +\mathrm{corr}(a_k,F_{x,i}),\qquad +\mathrm{corr}(a_k,F_{y,i}),\qquad +\mathrm{corr}(a_k,E_s) +\] + +同时做滞后版本: + +\[ +\mathrm{corr}(a_k(t),\Omega_i(t-\tau)),\qquad +\mathrm{corr}(a_k(t),E_s(t+\tau)) +\] + +如果 stealth 与 illusion 主要作用于同一对主模态,则说明它们可能是同一结构通道的不同目标化使用;如果作用对象不同,则可支持“机制并不相同”的判断。 + +## 任务 3 + +## observable-related decomposition + +你的控制目标仍然是 downstream signature shaping,但这一轮之后,OID 这条线不应再被理解成“对原始全流场做更高级模态分解”。更稳的做法是:**先把流场分成入射场、阻挡场和控制额外修正场,再在 correction field 上做 observable-related decomposition。** 也就是说,OID / PCD / CCD 的默认主对象应优先变成 + +\[ +\Delta q_{ctl} = q_{ctl} - q_{blk} +\] + +而不是直接拿 \(q_{ctl}\) 做解释。 + +同时,cloak 和 illusion 虽然都属于 signature shaping,但它们面对的原始流场和要完成的修正任务并不相同: + +- **steady cloak**:均匀来流下,pinball 自身会自激 shedding;控制后要把下游重新推回稳定均匀态。 +- **Karman cloak**:上游本来就有涡街;pinball 不控制时把它扰乱;控制后要恢复原来涡街的频率与形状。 +- **illusion**:均匀来流下,pinball 不控制时有 natural shedding;控制后要把这个 shedding 改写成目标圆柱的 shedding。 + +因此,这一部分不能再把 steady cloak、Karman cloak 和 illusion 当成同一种 OID 问题来处理。 + +### 3.1 为什么 POD 不够 + +POD 的排序标准是流场能量最大,而不是任务相关性最大 [Den18b, Tai17]。但对当前问题,真正关心的不是“哪个结构最强”,而是: + +- steady cloak 中,哪个 correction structure 最能压低 natural shedding、恢复均值尾迹 +- Karman cloak 中,哪个 correction structure 最能抵消 pinball 对 incoming street 的破坏 +- illusion 中,哪个 correction structure 最能把 natural pinball shedding 改写成 target shedding +- 哪个 structure 最影响总 force 或各圆柱 torque +- 哪个 structure 最决定 future signature + +因此必须比较三类坐标: + +- **POD 坐标**:最 energetic 的结构 +- **OID-style 坐标**:最影响 observable 的结构 [Sch12] +- **PCD-style 坐标**:与 observable 最相关、且可显式处理时延的结构 [Lyu23] + +如果后两者与 POD 前几阶不同,反而说明控制不是简单压主模态,而是在调少数 task-relevant correction structures。 + +### 3.2 前置数据准备 + +这一轮之后,OID 的前置数据不应只有公共 POD,还必须先有三场分解: + +- \(q_{in}\):入射参考场 +- \(q_{blk}\):固定 pinball 场 +- \(q_{ctl}\):控制 pinball 场 + +然后定义: + +\[ +\Delta q_{blk} = q_{blk} - q_{in} +\] + +\[ +\Delta q_{ctl} = q_{ctl} - q_{blk} +\] + +其中: + +- \(\Delta q_{blk}\) 代表 pinball 几何阻挡本身带来的被动扰动 +- \(\Delta q_{ctl}\) 代表旋转控制额外施加的主动修正 + +随后在 correction field 上做公共 POD。保留前 \(r\) 阶系数,建议先从 \(r=6,8,10\) 做最小 rank sensitivity,再根据数据长度扩到 \(r=12,16\)。定义 POD 系数向量: + +\[ +a(t_n)=\begin{bmatrix}a_1(t_n),\ldots,a_r(t_n)\end{bmatrix}^{\top} +\] + +对每个系数做标准化: + +\[ +\tilde a_k(t)=\frac{a_k(t)-\mu_{a_k}}{\sigma_{a_k}} +\] + +组成标准化矩阵 \(\tilde A\in\mathbb{R}^{N\times r}\)。observable 也全部做标准化,避免不同量纲主导分解。 + +### 3.3 三类场景下的 observable 设计 + +OID 现在不能只机械地区分 force observable 和 sensor observable,还要按场景定主问题。 + +#### A. steady cloak + +steady cloak 的第一问题不是“未来周期性 signature 匹配”,而是: + +- 压低自然 shedding +- 恢复均值尾迹 +- 收缩回流区 +- 让下游重新接近均匀稳定态 + +因此更适合的 observable 是: + +- 总 force / total torque +- control power +- fluctuation amplitude 或局部 RMS +- mean-wake 指标,如 \(L_r\)、\(A_r\)、尾迹中心线偏移 +- 下游相对于均匀稳态的误差 + +steady cloak 第一轮不必强行套入 periodic future-signature 模板。 + +#### B. Karman cloak + +Karman cloak 的第一问题是: + +- pinball 破坏了 incoming vortex street 的什么结构 +- 控制又额外施加了什么 correction 去恢复该 street +- 哪些结构最决定 future signature 的相位、频率和形状恢复 + +这里最自然的 observable 是: + +\[ +y_F(t)=\begin{bmatrix} +\sum_i F_{x,i}(t),\sum_i F_{y,i}(t),\sum_i M_{z,i}(t) +\end{bmatrix}^{\top} +\] + +以及 future-signature observable: + +\[ +p_{sig}(t)=\begin{bmatrix} +e_s(t+\tau_c), e_s(t+\tau_c+\tau), e_s(t+\tau_c+2\tau) +\end{bmatrix}^{\top} +\] + +其中 \(\tau_c\) 取控制到传感器的对流时延,\(\tau\) 建议先取约四分之一脱涡周期。 + +#### C. illusion + +illusion 的第一问题不是 suppress wake,而是 retune wake: + +- natural pinball shedding 如何被改写成 target shedding +- 哪些结构主要改频 +- 哪些结构主要改空间形态 +- 这些结构与 force 的关系是同一条通道,还是先近尾迹调制、后下游重组 + +这里同样优先保留: + +\[ +y_F(t)=\begin{bmatrix} +\sum_i F_{x,i}(t),\sum_i F_{y,i}(t),\sum_i M_{z,i}(t) +\end{bmatrix}^{\top} +\] + +以及 future-signature observable: + +\[ +p_{sig}(t)=\begin{bmatrix} +e_s(t+\tau_c), e_s(t+\tau_c+\tau), e_s(t+\tau_c+2\tau) +\end{bmatrix}^{\top} +\] + +必要时再补与目标 shedding 直接相关的低维指标,如主频偏差、phase error 或 envelope 差异。 + +### 3.4 从零开始的 OID-style 实现 + +严格按原始 OID 理论实现会比较重,对 coder 最稳的第一版仍然是 **cross-covariance OID-style decomposition**。但这一步默认应先作用在 correction-field POD 系数上,而不是 raw full-field POD 系数上。 + +给定标准化 correction-field POD 系数 \(\tilde A\in\mathbb{R}^{N\times r}\) 和标准化 observable 矩阵 \(\tilde Y\in\mathbb{R}^{N\times m}\),先计算交叉协方差: + +\[ +C_{AY}=\frac{1}{N}\tilde A^{\top}\tilde Y +\] + +对 \(C_{AY}\) 做奇异值分解: + +\[ +C_{AY}=U\Sigma V^{\top} +\] + +则: + +- \(U\) 的列向量给出 POD 子空间里与 observable 最相关的方向 +- 奇异值 \(\sigma_k\) 给出相关强度排序 +- 定义 observable-related 坐标 + +\[ +z^{OID}(t)=U^{\top}\tilde a(t) +\] + +- 对应空间模态可由 POD 模态线性组合得到 + +\[ +\psi_k^{OID}(x,y)=\sum_{j=1}^{r} U_{jk}\,\phi_j(x,y) +\] + +这一步的物理解释要写准:\(\psi_k^{OID}\) 不是最 energetic 的结构,而是最能改变当前选定 observable 的 **correction structure**。 + +#### OID-style 的主分支 + +1. **force-OID**:令 \(Y=y_F\) +2. **signature-OID**:令 \(Y=e_s\) 或 \(Y=p_{sig}\) +3. **steady-suppression OID**:steady cloak 下令 \(Y\) 取 fluctuation / mean-wake restoration 指标 + +这样回答的问题会更直接: + +- 哪些 correction structures 最影响 force +- 哪些 correction structures 最影响 future signature +- 哪些 correction structures 最能压低 natural shedding 并恢复稳定尾迹 + +### 3.5 从零开始的 PCD-style 实现 + +对当前问题,PCD 最值得用于处理 **时延 + 多变量 sensor + 噪声**。特别是 Karman cloak 与 illusion,它们都比 steady cloak 更依赖 future signature 的显式对齐。对 coder,推荐做一个 **delay-aware correlation decomposition**,本质上是对标准化 POD 系数和延迟 observable 做 whitening 后的 SVD。 + +先取标准化系数 \(\tilde A\) 和某个延迟 observable 矩阵 \(\tilde P\);例如 \(\tilde P=\tilde P_{sig}\) 或 \(\tilde P_{causal}\)。 + +定义协方差矩阵: + +\[ +C_{AA}=\frac{1}{N}\tilde A^{\top}\tilde A, +\qquad +C_{PP}=\frac{1}{N}\tilde P^{\top}\tilde P, +\qquad +C_{AP}=\frac{1}{N}\tilde A^{\top}\tilde P +\] + +构造 whitening 后的相关矩阵: + +\[ +K=C_{AA}^{-1/2} C_{AP} C_{PP}^{-1/2} +\] + +对 \(K\) 做 SVD: + +\[ +K=U\Sigma V^{\top} +\] + +定义 PCD-style 坐标: + +\[ +z^{PCD}(t)=U^{\top} C_{AA}^{-1/2}\tilde a(t) +\] + +对应空间模态仍由 POD 模态线性组合得到: + +\[ +\psi_k^{PCD}(x,y)=\sum_{j=1}^{r} W_{jk}\,\phi_j(x,y) +\] + +其中 \(W=C_{AA}^{-1/2}U\)。 + +这一步比 OID-style 更适合 periodic signature 问题,因为: + +- 可以自然纳入多时刻 observable +- 可以把当前 correction structure 与未来 signature 对齐 +- 对不同 observable 分量量纲和噪声更稳 [Lyu23] + +### 3.6 推荐的两套 PCD-style 版本 + +#### A. causal-PCD + +输入用 \(p_{causal}(t)\)。用途是: + +- 找到与当前控制输入最相关的结构态 +- 后续进入 `obs -> z -> act` + +#### B. signature-PCD + +输入用 \(p_{sig}(t)\)。用途是: + +- Karman cloak:找到哪些 correction structures 最决定 incoming street 的重建 +- illusion:找到哪些 correction structures 最决定 target shedding 的重建 +- 建立 `structure -> future signature` 链条 + +### 3.7 force-OID 与 signature-OID 的分工口径 + +这一轮之后,OID/CCD 这条线里必须长期保留一个区分:**force-relevant structures** 与 **signature-relevant structures** 不应先验地视为同一组 [Sch12, Kan17b, Lyu23]。 + +更稳的工作假设是: + +- controller 首先调制一小组与 force 强相关的 near-body / body-connected correction structures; +- 这些结构在若干个 \(D\) 的对流与相互作用后,再组织成与 downstream signature 强相关的 descendant structures。 + +因此,若后续分析发现 force-OID 与 signature-OID 不同,这不应被第一时间解释为算法失败;它很可能正是最有价值的机制结果。 + +### 3.8 该怎么判断 OID/PCD 比 POD 更有用 + +不能只看模态图好不好看,要用预测能力比较,并且要按场景选择评价对象。 + +#### 对比 1 + +用前 \(m\) 个 POD 坐标预测 observable,与用前 \(m\) 个 OID/PCD 坐标预测 observable 比较。 + +例如对 periodic cases: + +\[ +\hat e_s(t+\tau_c)=B z_{1:m}(t) +\] + +对 steady cloak,则可改用: + +- fluctuation suppression metric +- mean-wake restoration metric +- recirculation geometry metric + +#### 对比 2 + +用前 \(m\) 个坐标拟合动作: + +\[ +\hat \Omega_i(t)=b_i^{\top} z_{1:m}(t) +\] + +比较 POD 与 OID/PCD 的误差和稀疏度。 + +#### 对比 3 + +比较不同坐标与任务指标的相关强度: + +\[ +\mathrm{corr}(z_k,E_s),\qquad \mathrm{corr}(z_k,\Omega_i),\qquad \mathrm{corr}(z_k,y_F) +\] + +steady cloak 还应补看: + +\[ +\mathrm{corr}(z_k,L_r),\qquad \mathrm{corr}(z_k,A_r),\qquad \mathrm{corr}(z_k,\text{RMS metric}) +\] + +### 3.9 coder 的实现顺序 + +1. 先做三场分解,得到 \(q_{in}, q_{blk}, q_{ctl}\) +2. 构造 \(\Delta q_{blk}\) 与 \(\Delta q_{ctl}\) +3. 先做 correction-field POD,而不是直接做 raw-field POD +4. 标准化 \(a(t)\) 和 observable +5. 先做 force-OID +6. steady cloak 再做 suppression / mean-wake OID +7. Karman cloak 与 illusion 再做 signature-OID 或 signature-PCD +8. 输出前 3 到 5 个坐标和对应空间模态 +9. 比较这些坐标与 `act`、`force`、`future sensor error` 或 steady restoration 指标的关系 +10. 最后再决定是否追更严格的 paper-level exact OID/PCD 公式 + +### 3.10 输出要求 + +| 输出 | 说明 | 用途 | +|---|---|---| +| correction-field POD vs OID/PCD 模态对比 | 同一基底下比较 | 看最强 correction structure 与最相关 correction structure 是否不同 | +| \(z_k(t)\) 时间序列 | 各种坐标时序 | 看控制在调哪些 correction freedom | +| \(z_k\) 与 `act` 的回归图 | 含滞后版本 | 看白箱控制依赖何种结构态 | +| \(z_k\) 与 future signature 的关系 | 用 \(p_{sig}\) | 看哪些结构最决定周期性 cloak / illusion 成败 | +| \(z_k\) 与 suppression / recirculation 指标的关系 | steady cloak | 看哪些结构最决定稳态恢复 | +| 预测性能表 | POD / OID / PCD 三种坐标对比 | 证明 observable-related 分解有价值 | + +## 任务 4 + +## 白箱控制链的两级拟合 + +既然 stealth 下 `obs -> act` 已经可能较简单,就不要只停在这一条。应同时比较下列三类模型: + +\[ +\text{Model A:}\quad \Omega = f(s) +\] + +\[ +\text{Model B:}\quad z = g(s),\qquad \Omega = h(z) +\] + +\[ +\text{Model C:}\quad z = g(s,F),\qquad \Omega = h(z) +\] + +其中 \(f,g,h\) 可用线性回归、稀疏回归、SINDy 或简单符号回归。关键不是追求最高精度,而是比较: + +- 哪个模型最简单 +- 哪个模型最稳 +- 哪个模型更能解释结构态 + +### 4.1 单 case 白箱链 + +对单个 case,优先比较四种输入: + +- 原始 sensor:`s(t)` +- sensor + force:`[s(t), y_F(t)]` +- POD 坐标:`a(t)` +- OID/PCD 坐标:`z(t)` + +对每个圆柱拟合: + +\[ +\Omega_i(t)=h_i(\chi(t)) +\] + +其中 \(\chi\) 依次取上面四类输入,并比较精度与稀疏度。 + +### 4.2 针对涡街隐身的跨 Re 统一方程 + +对于上游圆柱涡街来流的 stealth 工况,建议单独做一条 **多 Reynolds 数统一方程** 路线。目标不是每个 case 单独给一个公式,而是检验是否存在一个共享骨架,在不同 \(Re_D\) 下只靠一个上下文参数就能内插 [Bru16, Loi17]。 + +这里最建议使用的上下文变量不是 \(Re\) 本身,而是 + +\[ +\mu = \frac{1}{Re_D} +\] + +或等价的无量纲黏性参数 \(\nu/(U_{ref}D)\)。原因是 Navier–Stokes 方程的黏性项本身就是按 \(1/Re\) 缩放,物理上比直接用 \(Re\) 更自然。 + +### 4.3 统一方程前的数据标准化 + +跨 Re 拟合前,必须先把不同 case 归一到同一坐标和时间标度,否则统一方程很容易被周期差和幅值差破坏。 + +建议统一做三步: + +1. **统一低维坐标** + - 所有 Re 的 vortex-street stealth case 用同一个公共 POD / OID / PCD 基底 + - 这样所有 case 的 \(z_k\) 都在同一个坐标系里 + +2. **统一时间标度** + - 用目标涡街的主频 \(f_{tar}\) 或脱涡周期 \(T_{tar}\) 归一化时间 + - 或者把每个周期重采样到相同点数,例如每周期 16 点 + +3. **统一幅值标度** + - sensor、force、\(z\) 坐标都做标准化 + - 推荐按各自 target case 的 RMS 做归一化,而不是按全局最大值 + +### 4.4 统一方程的候选形式 + +最稳的做法不是让每个 Re 拟合完全独立的式子,而是先固定一个共享特征库,再联合稀疏识别。 + +建议候选特征库先只包含低阶项: + +\[ +\Theta(t)=\Big[ +1,\mu, +z_1,\ldots,z_m, +y_{F,1},\ldots,y_{F,p}, +z_1^2,\ldots,z_m^2, +\mu z_1,\ldots,\mu z_m, +z_i z_j +\Big] +\] + +若考虑时延,则加入少量延迟项: + +\[ +z_k(t-\tau),\qquad y_{F,j}(t-\tau) +\] + +统一控制律写成 + +\[ +\Omega_i(t)=\beta_i^{\top}\Theta(t) +\] + +更具体一点,可写成 + +\[ +\Omega_i(t)=c_{i0}+c_{i\mu}\mu+\sum_{k=1}^{m}(c_{ik}+d_{ik}\mu)z_k(t) ++\sum_{j=1}^{p} b_{ij} y_{F,j}(t) ++\sum_{k\le l} q_{ikl} z_k(t)z_l(t) +\] + +这意味着: + +- 所有 Re 共用同一组 active terms +- 黏性影响主要通过 \(\mu\) 和 \(\mu z_k\) 来调节系数 + +### 4.5 联合拟合方式 + +把所有 Re case 的样本按行堆叠成一个大矩阵: + +\[ +\Omega_i^{all}=\Theta^{all}\beta_i +\] + +然后做联合稀疏回归。推荐顺序: + +1. 先普通 LASSO / sequential threshold least squares +2. 再做 group sparsity,让三个圆柱或多个 Re 共用项结构 +3. 最后再尝试符号回归做更漂亮的闭式表达式 + +实际目标不是一开始求最美公式,而是先验证: + +- 是否存在共享支持集 +- 是否只需一个 \(\mu\) 就能把多个 Re 串起来 + +### 4.6 必须做的验证 + +统一方程最关键的是验证“真内插”而不是只在训练点硬记住。必须至少做两类测试。 + +#### A. leave-one-Re-out + +例如用三个 Re 训练,留一个 Re 测试,看统一方程是否还能保持合理误差。 + +#### B. pairwise interpolation + +例如只用低 Re 和高 Re 训练,测试中间 Re。若中间 Re 也能拟合,说明 \(\mu\) 的连续插值是有意义的。 + +### 4.7 成功与失败的判据 + +统一方程成功,不等于误差最低,而是满足: + +- active term 数量明显少于逐 case 单独拟合后项数总和 +- leave-one-Re-out 误差没有灾难性变差 +- 主要 active terms 在不同 Re 下仍是同一批 +- 系数随 \(\mu\) 的变化平滑、可解释 + +如果做不到,则不要强行保留“一个总公式”的说法,而应退一步写成: + +- **统一骨架 + Re 依赖系数** + +或者: + +- **低 Re 与高 Re 两个分段方程** + +### 4.8 推荐的统一拟合版本顺序 + +1. 先做单 case 的 `obs -> act` +2. 再做单 case 的 `z -> act` +3. 再做多 Re 的 `z, y_F, \mu -> act` +4. 最后再尝试 raw `obs, \mu -> act` + +如果 `z, y_F, \mu -> act` 比 raw `obs, \mu -> act` 更简单、更稳,就能非常有力地说明: + +- 不同 Re 的控制不是完全不同的策略 +- 而是在同一低维结构反馈骨架上,受黏性参数 \(\mu\) 调制 + +### 判据 + +| 判据 | 说明 | +|---|---| +| 拟合误差 | `RMSE`, `MAE`, `R^2` | +| 稀疏度 | 非零项数 | +| 稳定性 | 不同 case 或不同时间窗是否保结构 | +| 内插性 | leave-one-Re-out 是否成立 | +| 物理性 | 是否能对应少数结构态 | + +如果出现 `obs -> act` 复杂、但 `obs -> z -> act` 简单的情况,这会是很强的物理论据 [Loi17, Bru16, Li22b]。 + +## 任务 5 + +## 能量分析的三层实现 + +### 5.1 第一层 + +最小功率 bookkeeping + +这是必须完成的最低层。 + +输出: + +- 每个圆柱 \(P_{c,i}(t)\) +- 总功率 \(P_c(t)\) +- 平均功率、RMS、谱 +- 功率与 DTW 改善的关系 + +建议再加两个效率指标: + +\[ +\eta_{DTW} = \frac{\Delta \mathrm{Sim}}{\overline{P_c}+\epsilon} +\] + +\[ +\eta_{force} = \frac{\Delta F_{rms}}{\overline{P_c}+\epsilon} +\] + +其中 \(\epsilon\) 为防止分母为零的小量。 + +### 5.2 第二层 + +mean–fluctuation 动能 + +定义平均动能 + +\[ +K_m = \int_{\Omega} \frac{1}{2}(\bar u^2+\bar v^2)\,d\Omega +\] + +定义脉动动能 + +\[ +K_f = \int_{\Omega} \frac{1}{2}\overline{u'^2+v'^2}\,d\Omega +\] + +若数据质量允许,定义黏性耗散近似 + +\[ +\varepsilon_f \approx \nu \int_{\Omega} \overline{\nabla \mathbf u' : \nabla \mathbf u'}\,d\Omega +\] + +\[ +\varepsilon_m \approx \nu \int_{\Omega} \nabla \bar{\mathbf u} : \nabla \bar{\mathbf u}\,d\Omega +\] + +这层的关键判断是: + +- stealth 是否主要降低 \(K_f\) +- illusion 是否在不显著降低 \(K_f\) 的情况下重组结构 +- disturbed inflow 是否主要增加 \(\varepsilon_f\) 或功率波动 + +### 5.3 第三层 + +coherent transfer 与频带耦合 + +这一层是可选增强。建议先做轻量版,再决定是否上 BMD [Sch20]。 + +轻量版顺序: + +1. 传感器和主模态系数 FFT +2. 探针信号 bispectrum +3. 主频、二倍频、均值分量之间的相干与相位 + +若这些结果明确显示二次耦合,再考虑 full BMD。对当前数据,直接上 BMD 不是第一优先级。 + +## 任务 6 + +## 回流区与通量分析 + +受控 wake 很多时候可以通过回流区通量平衡来解释,而不仅是“能量变小了” [Kha24c]。 + +### 6.1 回流区边界 + +用均值流的 \(\bar u=0\) 等值线作为 recirculation region interface。 + +### 6.2 通量量 + +对边界曲线 \(\Gamma_r\) 上的单位法向量 \(\mathbf n\),定义法向通量 + +\[ +\Phi_r = \int_{\Gamma_r} \bar{\mathbf u}\cdot \mathbf n\, ds +\] + +在数值上,对外流与内流分别积分: + +\[ +\Phi^{+} = \int_{\Gamma_r} \max(\bar{\mathbf u}\cdot\mathbf n,0)\, ds +\] + +\[ +\Phi^{-} = \int_{\Gamma_r} \min(\bar{\mathbf u}\cdot\mathbf n,0)\, ds +\] + +同时可在上下 shear layer 位置定义辅助界面,统计各自通量。目标不是做 3D bluff-body 那样完整的体积分,而是先比较: + +- stealth 是否让上下通量更平衡 +- illusion 是否让回流区形态和通量分布向 target 靠拢 +- 某些控制是否通过延后 shear-layer roll-up 改变 bubble 长度 + +## 任务 7 + +## circular body-force probing + +局部体力 probing 的目标不是替代 pinball 旋转,而是做 **结构敏感性和因果诊断**。已有圆柱尾迹的灵敏度、adjoint、wavemaker 和 localized resolvent 结果表明:局部 forcing 的空间位置、方向和频率本身就能提供很强的结构信息 [Gia07, Mar08b, Jin19b, Jin21, Ske22]。 + +### 7.1 圆形 forcing 的统一定义 + +对第 \(m\) 个圆形区域,中心为 \((x_m,y_m)\),半径为 \(R_m\)。定义空间窗函数 + +\[ +\phi_m(x,y)= +\begin{cases} +\frac{1}{2}\left[1+\cos\left(\pi r_m/R_m\right)\right], & r_m \le R_m \\ +0, & r_m > R_m +\end{cases} +\] + +其中 \(r_m = \sqrt{(x-x_m)^2+(y-y_m)^2}\)。 + +则局部体力写为 + +\[ +\mathbf f_p(x,y,t)=A\,\phi_m(x,y)\,g(t)\,\mathbf e_d +\] + +其中 \(\mathbf e_d\in\{\mathbf e_x,\mathbf e_y\}\)。 + +### 7.2 第一轮 probing 类型 + +只做两类: + +1. 脉冲 forcing + +\[ +g(t)=\mathbb{1}_{[t_0,t_0+\Delta T]}(t) +\] + +2. 单频谐波 forcing + +\[ +g(t)=\sin(2\pi f t) +\] + +第一轮频率只扫: + +- 主脱涡频率 \(f_s\) +- 目标频率 \(f_{tar}\) +- \(2f_s\) +- \(2f_{tar}\) + +### 7.3 候选圆区 + +第一轮不做全场遍历,先选最有物理意义的 5 类区域: + +| 区域 | 位置建议 | 目的 | +|---|---|---| +| 前圆柱后缘近尾迹 | front cylinder 后方 | 看主剪切层起始敏感性 | +| 两后圆柱之间 | pinball 中央空隙 | 看 central jet / 对称性通道 | +| 上剪切层起始处 | 上后圆柱外侧 | 看反对称结构 | +| 下剪切层起始处 | 下后圆柱外侧 | 看反对称结构 | +| pinball 后方主尾迹核心 | 三圆柱后方短距离 | 看回流区与大尺度 roll-up | + +### 7.4 probing 输出 + +每次 probing 必须输出: + +| 输出 | 指标 | +|---|---| +| 传感器响应 | \(\max_t \lVert \Delta s(t) \rVert\)、窗口化 DTW 变化 | +| 受力响应 | 总 lift、drag、各圆柱 torque 峰值变化 | +| 结构态响应 | 主 POD/OID 系数峰值与恢复时间 | +| 做功 | \(P_f(t)=\int_{\Omega} \mathbf u\cdot\mathbf f_p\,d\Omega\) | +| 频域响应 | 传感器与主模态的谱峰变化 | + +定义累计 forcing 做功 + +\[ +W_f = \int_{t_0}^{t_1} P_f(t)\,dt +\] + +再定义响应效率 + +\[ +\eta_s = \frac{\max_t \lVert \Delta s(t) \rVert}{W_f+\epsilon} +\] + +\[ +\eta_z = \frac{\max_t \lvert \Delta z_k(t) \rvert}{W_f+\epsilon} +\] + +最终产出应包括: + +- 区域–方向 敏感性热图 +- 区域–频率 响应图 +- 若干代表区域的时序响应图 + +## 任务 8 + +## 图组与最终交付物 + +建议按下面顺序生成标准图,先做低风险主图,再做增强图。 + +| 顺序 | 图 | 内容 | +|---|---|---| +| 1 | mean + RMS 图 | uncontrolled / stealth / illusion / target 对比 | +| 2 | 回流区图 | \(\bar u=0\) 边界、长度、面积、对称性 | +| 3 | force / torque / power 图 | 三圆柱局部分工与总功率 | +| 4 | POD 图 | 公共基底、模态能量、系数相图 | +| 5 | observable-related 图 | OID/PCD 模态和 \(z(t)\) | +| 6 | 白箱链图 | `obs -> act` vs `obs -> z -> act` | +| 7 | probing 热图 | 区域–方向–频率 敏感性 | +| 8 | 频带耦合图 | FFT / bispectrum / 必要时 BMD | + +## coder 的执行顺序 + +最稳妥的实现顺序如下。 + +1. 做统一数据读取与同步校验 +2. 生成 mean、RMS、vorticity、force、power 的基础结果 +3. 提取回流区边界与几何统计 +4. 做公共 POD 和主系数关联图 +5. 构造 observable,做 OID/PCD 或相关替代方案 +6. 先补做三场分解:`q_in`, `q_blk`, `q_ctl`,并明确 `Δq_blk` 与 `Δq_ctl` +7. 在 correction field 上检查 cloak 是否呈现 wake cancellation、illusion 是否呈现 target-oriented correction +8. 比较 `obs -> act` 与 `obs -> z -> act` +9. 实现最小版能量分析:\(P_c\)、\(K_m\)、\(K_f\)、必要时 \(\varepsilon_f\) +10. 实现圆形体力 probing 的脉冲与谐波扫描 +11. 最后视结果决定是否进入 bispectrum / BMD + +## 直接判断成功与否的标准 + +如果后处理结果支持以下判断,则整套方案算成功: + +- stealth 主要通过压低与传感器误差最相关的结构并重构均值尾迹实现 [Sch12, Lyu23, Tad10] +- illusion 不是简单降能,而是保留或重组目标相关结构与频带 [Jin20, Sch20] +- 三圆柱在力、力矩和功率上存在可识别分工 [Ber06, Den20] +- 控制对白箱化后,简单律主要作用于少数低维结构态,而不是直接依赖全场 [Loi17, Bru16, Li22b] +- 若三场分解成立,则控制主要体现为对 pinball-scale correction field 的低维调制,而不是简单的单一力学量跟踪 +- body-force probing 能识别出少数高敏感区域,并与真实控制下的主要结构变化相呼应 [Gia07, Mar08b, Jin21] +- force 最好被解释为 body-connected near-wake organization 的低维投影,而不是与涡量竞争的另一套机制 [Kan17b, Geh23, Sch12] +- force-relevant structures 与 signature-relevant structures 若被区分出来,应视为加强主线的结果,而不是分析失败 [Sch12, Lyu23] + +这套任务书的重点不是让所有高阶分析都必须成功,而是保证第一轮就能产出一组稳的、可解释的、能直接服务论文主线的结果。 \ No newline at end of file diff --git a/src/drl_pinball/reproduce/REPRODUCE_KNOWLEDGE.md b/src/drl_pinball/reproduce/REPRODUCE_KNOWLEDGE.md new file mode 100644 index 0000000..2be7424 --- /dev/null +++ b/src/drl_pinball/reproduce/REPRODUCE_KNOWLEDGE.md @@ -0,0 +1,300 @@ +# Reproduction Knowledge Document + +> **Purpose**: Complete record of all experience, pitfalls, and findings from reproducing legacy DRL pinball control results on the new CelerisLab CFD solver. +> **Date**: 2026-06-21 (all phases completed, 7 scenes tested) +> **Next step**: Train new PPO models from scratch on the new CelerisLab solver. The scripts `run_all_cases.py` (Karman/Steady) and `run_illusion_vortex.py` (Illusion/Vortex) in this directory serve as reference implementations for building training environments. + +--- + +## 1. Overall Picture + +### What was attempted + +Take old PPO models (trained on `LegacyCelerisLab/FlowField`) and run them on the new `CelerisLab/Simulation` solver, comparing sensor signals, forces, normalized observations, DTW similarity, and reward values against `SR_analysis` reference data. + +### Conclusion + +**Old PPO models cannot be perfectly transferred to the new CelerisLab solver.** Two independent LBM implementations produce systematically different plant dynamics (especially in vortex-dominated wake regions). Retraining on the new solver is the only complete solution. + +### Best results achieved (Karman Cloak, legacy norm) + +| Metric | Our result | Reference | Coverage | +|--------|:----------:|:---------:|:--------:| +| Action correlation (aF/aB/aT) | +0.83 / +0.81 / +0.63 | — | Good | +| Front_fy correlation | +0.90 | — | Excellent | +| DTW similarity | 0.916 | 0.954 | 96% | +| Sensor correlation | 0.69 ~ 0.72 | — | Moderate | +| Reward | 0.412 | 0.644 | 64% | + +### All scenes summary + +| Scene | DTW similarity | Action corr | Key issue | +|-------|:--------------:|:-----------:|-----------| +| Steady Cloak | N/A (open-loop) | N/A | Fully works | +| Karman Cloak (legacy norm) | 0.916 | 0.63-0.83 | Rear fy std 6x larger | +| Karman Cloak (new norm) | poor | -0.07~-0.04 | Uncorrelated actions | +| Illusion 1L (legacy norm) | 0.912 | 0.29-0.52 | Moderate correlation | +| Illusion 0.75L (legacy norm) | 0.926 | -0.32~+0.50 | Rear action sign inverted | +| Illusion 1.5L (legacy norm) | 0.894 | 0.08-0.15 | Low correlation | +| Vortex Lamb (legacy norm) | 0.955 | -0.22~-0.26 | Norm 16% diff | +| Vortex Taylor (legacy norm) | 0.979 | 0.01-0.39 | Norm 60% diff | + +--- + +## 2. Critical API Differences + +### 2.1 Omega: Surface Velocity vs Angular Velocity + Sign Inversion + +**This was the most impactful bug.** + +| Solver | Kernel code | Meaning | +|--------|------------|---------| +| **Legacy** `FlowField` | `Uw = action[id_obj] * (y_c - y) / radius` | action = tangential surface velocity | +| **New** `Simulation` | `Uw = -omega * ry` | `set_body(id, omega=val)` = angular velocity | + +The new CelerisLab kernel has `Uw = -omega * ry` — **the leading minus sign means `omega > 0` produces CW rotation**, opposite to the documented convention. The old `FlowField` has no minus sign. + +**Corrected conversion**: +```python +omega = -surface_vel / radius +``` +Where `surface_vel = (action_norm * scale + bias) * U0` and `radius = L0/2 = 10`. + +### 2.2 Sensor Area Normalization + +| Solver | Physical meaning | Value | +|--------|-----------------|-------| +| **Legacy** `obs[i]` | `sum_cells(u) / steps` | No area division | +| **New** `read_sensor(id, normalize=True)` | `sum_cells(u) / (cell_count * steps)` | Area- AND time-averaged | + +**Conversion**: `legacy_equiv = new_value * cell_count` + +Sensor cell count for radius=5: **78 cells** (verified empirically). + +Key insight: if using `legacy norm` with new sensor values, you MUST convert sensors to old-equiv first: +```python +sensor_old_equiv = sim.read_sensor(id, normalize=True) * 78.0 +``` + +### 2.3 Action Smoothing + +Legacy `FlowField.run()` has built-in exponential smoothing with `weight=0.1`: +``` +pinned = 0.9 * pinned + 0.1 * target +``` + +New CelerisLab has NO built-in smoothing. Must implement manually. + +**Critical: EMA must be initialized to the bias values, NOT zeros.** +In legacy code, `self.action` retains the last bias value after the bias FIFO phase, so the first DRL inference step starts from a smoothed bias state: + +```python +ema = EMA(weight=0.1) +ema.reset(bias_omega.copy()) # NOT ema.reset(np.zeros(3)) +``` + +### 2.4 Snapshot/Restore Timing + +**The snapshot must be taken AFTER the bias FIFO, not before.** + +Legacy `cfd_interface.py` `add_pinball()` does: +1. Stabilize pinball (zero action) +2. `get_ddf()` + `save_ddf()` — saves zero-action state temporarily for norm +3. Collect norm, apply_bias, then does `get_ddf()` + `save_ddf()` again — **overwrites DDF with bias-state** +4. So `reset()` → `restore_ddf()` goes to **post-bias** state + +Correct behavior: +```python +sim.run(warmup) # stabilize +sim.snapshot() # OPTIONAL: temp save for norm +# ... collect norm ... +sim.restore() # back to pre-bias state +# ... bias FIFO ... +sim.snapshot() # OVERWRITE: now at post-bias state +# DRL inference starts here: +sim.restore() # goes to post-bias state +``` + +### 2.5 Runtime Body Addition (sync_bodies) + +```python +n_before = sim.bodies.count +sim.add_body("circle", center=(x, y, 0.0), radius=r) # returns -1 (staged) +sim.sync_bodies() # commit +pinball_ids = list(range(n_before, n_before + 3)) # real IDs discovered AFTER sync +``` + +### 2.6 PyCUDA + PyTorch Context Conflict + +NVIDIA V100 has only one CUDA context per process. Solution: load PPO model on CPU: +```python +model = PPO.load(path, env=dummy_env, device="cpu") # CPU avoids context conflict +``` + +--- + +## 3. Body ID and Obs Layout + +### 3.1 Karman Cloak (7 objects: dist_cyl + 3 sensors + 3 pinball) + +**Add order**: dist_cyl(0) → sensor0(1) [y=+2L0] → sensor1(2) [y=0] → sensor2(3) [y=-2L0] → front(4) → top(5) [y=+0.75L0] → bottom(6) [y=-0.75L0] + +**Legacy obs** (14 values): +``` +[0:2] = dist_cyl fx, fy +[2:8] = sensor0_ux,uy, sensor1_ux,uy, sensor2_ux,uy +[8:14] = front_fx,fy, top_fx,fy, bottom_fx,fy +``` +**Training uses** `obs[2:14]` → skips dist_cyl forces. + +**Action -> body mapping**: +| Action index | Bias (U0 mult) | Body ID | Cylinder | +|:-----------:|:--------------:|:-------:|----------| +| 0 | 0 | 4 | Front | +| 1 | -4 | 5 | Top (rear, +y) | +| 2 | +4 | 6 | Bottom (rear, -y) | + +**Normalized obs order**: `[forces(6)/norm, sensors(6)/norm]` clipped [-1,1]. + +### 3.2 Illusion/Vortex (6 objects: 3 sensors + 3 pinball) + +**Add order**: sensor0(0) → sensor1(1) → sensor2(2) → front(3) → top(4) → bottom(5) + +**Obs**: `[s0_ux,uy, s1_ux,uy, s2_ux,uy, front_fx,fy, top_fx,fy, bottom_fx,fy]` +Training uses `obs[0:12]` (all channels). + +**Normalized obs order (S_DIM=12)**: same as Karman: forces first, sensors second. +**Normalized obs order (S_DIM=14, Illusion)**: forces(6) + sensors(6) + target_cd(1) + target_cl(1) + +--- + +## 4. Scene Parameters + +### 4.1 Scene settings + +| Scene | S_DIM | Action scale | Action bias | SI | CONV_LEN | MaxSteps | Objects | +|-------|:----:|:------------:|:-----------:|:--:|:--------:|:--------:|:-------:| +| Steady Cloak | 12 | 8 | [0, -5.1, 5.1] | 800 | 30 | 500 | 6 | +| Karman Cloak | 12 | 8 | [0, -4, 4] | 800 | 30 | 500 | 7 | +| Illusion 0.75L | 14 | 8 | [0, -2, 2] | 400 | 36 | 500 | 6 | +| Illusion 1L | 14 | 8 | [0, -2, 2] | 600 | 36 | 500 | 6 | +| Illusion 1.5L | 14 | 8 | [0, -2, 2] | 800 | 36 | 500 | 6 | +| Vortex Lamb | 12 | 4 | [0, -4, 4] | 800 | 30 | 150 | 6 | +| Vortex Taylor | 12 | 4 | [0, -4, 4] | 800 | 30 | 150 | 6 | + +### 4.2 Bias init actions (for FIFO initialization - may differ from DRL bias!) + +| Scene | Init bias (surface_vel) | DRL bias | +|-------|:----------------------:|:--------:| +| Karman | [0, -4, 4] * U0 | Same | +| Illusion | **[0, -1, 1] * U0** | [0, -2, 2] * U0 | +| Vortex | [0, -4, 4] * U0 | Same | + +### 4.3 Norm formulas + +All scenes use: +- `force_norm_fact = 6 * max(|forces|)` +- `sens_deviation[i] = mean(sensor_i)` +- `sens_norm_fact[i] = 5 * max(|sensor_i - mean|)` + +### 4.4 Omega conversion per scene + +```python +# Karman, Steady, Illusion: +target_omega = -(action * 8.0 + bias) * U0 / 10.0 + +# Vortex: +target_omega = -(action * 4.0 + bias) * U0 / 10.0 +``` + +--- + +## 5. All Bugs Found & Fixed + +| # | Bug | Symptom | Fix | +|---|-----|---------|-----| +| **1** | Omega sign inverted | force fy means wrong sign, front_fy anti-correlated (-0.97) with ref actions open-loop | `omega = -surface_vel / R` | +| **2** | Snapshot before bias FIFO | Inference starts from zero-action flow, DRL sees wrong obs | `sim.snapshot()` after bias FIFO | +| **3** | EMA initialized to zeros | First DRL step applies 10% of target action = no effect | `ema.reset(bias_omega)` | +| **4** | Sensor not converted to old-equiv | Sensors have intrinsic area normalization, old norm doesn't expect it | `new_sensor * cell_count(78)` | +| **5** | Wrong body ID after sync_bodies | `set_body(-1, ...)` raises KeyError | `pinball_ids = list(range(n_before, n_before+3))` | +| **6** | `snapshot()` before `restore()` | `RuntimeError: No snapshot to restore` | Always snapshot after warmup | + +--- + +## 6. File Structure (after cleanup) + +``` +reproduce/ +├── __init__.py +├── REPRODUCE_KNOWLEDGE.md ← This file +├── core/ +│ ├── __init__.py +│ ├── action_wrapper.py # EMA smoother + omega conversion +│ ├── obs_normalizer.py # Norm computation helpers +│ └── dtw_metrics.py # DTW similarity + harmonics +├── configs/ +│ ├── __init__.py +│ ├── scene_params.py # All scene parameter definitions +│ └── model_inventory.py # Model loading (DummyEnv + Sin) +├── run_all_cases.py # Steady + Karman Cloak reproduction +├── run_illusion_vortex.py # Illusion + Vortex reproduction +└── output/ + ├── PHASE4_SUMMARY.md # Cross-scene comparison report + ├── steady_cloak/ # Final steady cloak output + ├── karman_cloak/ # Final Karman cloak output + ├── illusion_1L/ # Final Illusion 1L output + ├── illusion_075L/ # Final Illusion 0.75L output + ├── illusion_15L/ # Final Illusion 1.5L output + ├── vortex_lamb/ # Final Vortex Lamb output + └── vortex_taylor/ # Final Vortex Taylor output +``` + +--- + +## 7. Reference Data Locations + +| Scene | Path | +|-------|------| +| Karman | `src/SR_analysis/data/karman/karman_re100/` | +| Illusion 1L | `src/SR_analysis/data/illusion/illusion_1L/` | +| Illusion 0.75L | `src/SR_analysis/data/illusion/illusion_0.75L/` | +| Illusion 1.5L | `src/SR_analysis/data/illusion/illusion_1.5L/` | +| Vortex Lamb | `src/SR_analysis/data/vortex/vortex_lamb/` | +| Vortex Taylor | `src/SR_analysis/data/vortex/vortex_taylor/` | +| Steady | `src/SR_analysis/data/steady/steady/` | + +Each contains: `controlled.npz`, `uncontrolled.npz` (Karman only), `target.npz`, `norm.json`, `config.json`, `result.json` + +--- + +## 8. Recommended Workflow for New Training + +``` +1. Create env with new CelerisLab Simulation + ├─ Add bodies in legacy order + ├─ initialize(), warmup (4*NX/U0 steps) + ├─ Record target (150 x SI steps, sensors converted to old-equiv) + ├─ Add pinball via sync_bodies() + ├─ Warmup pinball (zero action) + ├─ Collect zero-action FIFO → compute norm + ├─ Bias FIFO (EMA smoother, proper bias values) + ├─ sim.snapshot() ← AFTER bias FIFO! + └─ save save_states + +2. Create gym.Env wrapper (observation_space, action_space, step, reset) + ├─ step(): apply_action_smoothed → read_obs → normalize → build_obs → compute_reward + ├─ reset(): sim.restore(), fifo = save_states.copy() + └─ reward: same formula as legacy (DTW + force terms) + +3. Train PPO (same as legacy) + ├─ Network: MlpPolicy, Sin activation, 64×64 + ├─ PPO hyperparams: lr=3e-4(actor)/4e-4(critic), n_steps=2048, batch_size=64 + ├─ Train for 500-1000 episodes (360-1500 timesteps per iter) + └─ Load PPO model on CPU (avoids CUDA context conflict) + +4. Evaluate + ├─ Run deterministic inference + ├─ Compare reward curves across seeds + └─ Check DTW similarity against target +``` diff --git a/src/drl_pinball/reproduce/__init__.py b/src/drl_pinball/reproduce/__init__.py new file mode 100644 index 0000000..d453711 --- /dev/null +++ b/src/drl_pinball/reproduce/__init__.py @@ -0,0 +1,13 @@ +"""Reproduction package. + +Purpose: Attempted reproduction of legacy DRL pinball control on new CelerisLab API. + +Status: Legacy models do NOT work on new solver due to ~4% flow profile difference. +See REPRODUCE_KNOWLEDGE.md for full findings and guidance. + +Contents: +- core/ : Verified utility modules (action wrapper, obs normalizer, DTW) +- configs/ : Scene parameters and model inventory +- inference/ : DRL inference script (Karman cloak) +- validation/ : Layer-by-layer validation against SR_analysis reference +""" diff --git a/src/drl_pinball/reproduce/configs/__init__.py b/src/drl_pinball/reproduce/configs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/drl_pinball/reproduce/configs/model_inventory.py b/src/drl_pinball/reproduce/configs/model_inventory.py new file mode 100644 index 0000000..7a0c38f --- /dev/null +++ b/src/drl_pinball/reproduce/configs/model_inventory.py @@ -0,0 +1,133 @@ +"""Model inventory — metadata and loading utilities for all PPO models. + +Provides: +- ModelInventory: registry of all pre-trained PPO models +- DummyEnv: minimal SB3-compatible env for PPO.load() +""" +from __future__ import annotations + +import os +from typing import Any, Dict, Optional + +import numpy as np +import gymnasium as gym +from gymnasium import spaces +from stable_baselines3 import PPO +from torch.nn import Module as TorchModule + +_PROJECT_ROOT = os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "..", "..", "..") +) +_MODELS_DIR = os.path.join(_PROJECT_ROOT, "models") + + +# --------------------------------------------------------------------------- +# DummyEnv for model loading +# --------------------------------------------------------------------------- +class DummyEnv(gym.Env): + """Minimal SB3-compatible environment for loading PPO models. + + PPO.load() requires an env (or env's observation_space / action_space). + This dummy env provides the correct spaces without any CFD backend. + """ + + def __init__(self, s_dim: int = 12, a_dim: int = 3): + super().__init__() + self.observation_space = spaces.Box( + low=-1.0, high=1.0, shape=(s_dim,), dtype=np.float32, + ) + self.action_space = spaces.Box( + low=-1.0, high=1.0, shape=(a_dim,), dtype=np.float32, + ) + + def reset(self, *, seed=None, options=None): + return np.zeros(self.observation_space.shape, dtype=np.float32), {} + + def step(self, action): + obs = np.zeros(self.observation_space.shape, dtype=np.float32) + return obs, 0.0, False, False, {} + + +# --------------------------------------------------------------------------- +# Custom Sin activation (must match training) +# --------------------------------------------------------------------------- +class Sin(TorchModule): + def __init__(self): + super().__init__() + + def forward(self, x): + import torch + return torch.sin(x) + + +# --------------------------------------------------------------------------- +# Model metadata +# --------------------------------------------------------------------------- +MODEL_META: Dict[str, Dict[str, Any]] = { + # Old models: Karman cloak, cross-Re + "d1a3o12_re50": {"scene": "karman_cloak_re50", "s_dim": 12, "subdir": "old"}, + "d1a3o12_re100": {"scene": "karman_cloak_re100", "s_dim": 12, "subdir": "old"}, + "d1a3o12_re200": {"scene": "karman_cloak_re200", "s_dim": 12, "subdir": "old"}, + "d1a3o12_re400": {"scene": "karman_cloak_re400", "s_dim": 12, "subdir": "old"}, + # Vortex (transfer-learned from re100) + "vortex_lamb": {"scene": "vortex_lamb", "s_dim": 12, "subdir": "old"}, + "vortex_taylor": {"scene": "vortex_taylor", "s_dim": 12, "subdir": "old"}, + # Re-trained cloak 250326 + "d1a3o12_250326": {"scene": "karman_cloak_re100", "s_dim": 12, "subdir": "250326"}, + # No-offset cloak + "d0a3o12_250329_nooffset": {"scene": "karman_cloak_re100", "s_dim": 12, "subdir": "250329"}, + # Reduced obs + "d1a3o12_250421_forces02": {"scene": "karman_cloak_re100", "s_dim": 3, "subdir": "250421"}, + "d1a3o12_250421_torque+forces02": {"scene": "karman_cloak_re100", "s_dim": 5, "subdir": "250421"}, + "d1a3o12_250421_torque+forces02+sens24": {"scene": "karman_cloak_re100", "s_dim": 9, "subdir": "250421"}, + "d1a3o12_250421_torque+forces04+sens04": {"scene": "karman_cloak_re100", "s_dim": 5, "subdir": "250421"}, + "d1a3o12_250421_torque+total_force": {"scene": "karman_cloak_re100", "s_dim": 5, "subdir": "250421"}, + "d1a3o12_250421_total_force": {"scene": "karman_cloak_re100", "s_dim": 3, "subdir": "250421"}, + # Illusion (250525) + "d1a3o14_250525_imit_075L_2U_400S": {"scene": "illusion_075L", "s_dim": 14, "subdir": "250525"}, + "d1a3o14_250525_imit_1L_2U_600S": {"scene": "illusion_1L", "s_dim": 14, "subdir": "250525"}, + "d1a3o14_250525_imit_15L_2U": {"scene": "illusion_15L", "s_dim": 14, "subdir": "250525"}, + # Additional illusion variants (useful for testing) + "d1a3o14_250525_imit_075L_2U": {"scene": "illusion_075L", "s_dim": 14, "subdir": "250525"}, + "d1a3o14_250525_imit_1L_2U": {"scene": "illusion_1L", "s_dim": 14, "subdir": "250525"}, + "d1a3o14_250525_imit_1L_2U_trans": {"scene": "illusion_1L", "s_dim": 14, "subdir": "250525"}, + "d1a3o14_250525_imit_1L_2U_1000S_08Vis": {"scene": "illusion_1L", "s_dim": 14, "subdir": "250525"}, + "d1a3o14_250525_imit_1L_2U_800S_08Vis": {"scene": "illusion_1L", "s_dim": 14, "subdir": "250525"}, + "d1a3o14_250525_imit_1L_2U_400S_02Vis": {"scene": "illusion_1L", "s_dim": 14, "subdir": "250525"}, + "d1a3o14_250525_imit_075L_2U_1": {"scene": "illusion_075L", "s_dim": 14, "subdir": "250525"}, + "d1a3o14_250525_imit_075L_2U_400S": {"scene": "illusion_075L", "s_dim": 14, "subdir": "250525"}, + "d1a3o14_250525_imit_15L_2U": {"scene": "illusion_15L", "s_dim": 14, "subdir": "250525"}, + # Erase models (for reference, not primary focus) + "d1a3o12_250729_250326_erase": {"scene": "karman_cloak_re100", "s_dim": 12, "subdir": "250729"}, + "d1a3o12_250729_250326_erase_250804_20D_retrain2": {"scene": "karman_cloak_re100", "s_dim": 12, "subdir": "250729"}, + "d1a3o12_250729_250326_erase_250804_20D_retrain3": {"scene": "karman_cloak_re100", "s_dim": 12, "subdir": "250729"}, +} + + +class ModelInventory: + """Registry for loading pre-trained PPO models.""" + + def __init__(self): + self.models_dir = _MODELS_DIR + + def get_model_path(self, name: str) -> str: + if name not in MODEL_META: + raise KeyError(f"Unknown model '{name}'") + meta = MODEL_META[name] + return os.path.join(self.models_dir, meta["subdir"], f"{name}.zip") + + def load(self, name: str, device: str = "cuda:0") -> PPO: + """Load a PPO model with correct observation space and Sin activation.""" + if name not in MODEL_META: + raise KeyError(f"Unknown model '{name}'") + meta = MODEL_META[name] + dummy = DummyEnv(s_dim=meta["s_dim"]) + path = self.get_model_path(name) + model = PPO.load(path, env=dummy, device=device) + return model + + def list_models(self, scene: Optional[str] = None) -> list: + """List model names, optionally filtered by scene name.""" + if scene is None: + return sorted(MODEL_META.keys()) + return sorted(k for k, v in MODEL_META.items() if v["scene"] == scene) diff --git a/src/drl_pinball/reproduce/configs/scene_params.py b/src/drl_pinball/reproduce/configs/scene_params.py new file mode 100644 index 0000000..a234830 --- /dev/null +++ b/src/drl_pinball/reproduce/configs/scene_params.py @@ -0,0 +1,241 @@ +"""All scene parameters in one place. + +Single source of truth for geometry, action scaling, norm formulas, and DRL settings. +Every scene family needed for reproduction is defined here. + +Re convention: + - "re_code" uses reference length 2*D (=40 lattice units), matching model file naming. + - nu = U0 * (2*D) / re_code + - Physical Re_D = re_code / 2 (uses single cylinder diameter D=20) +""" +from __future__ import annotations + +from typing import Any, Dict + +import numpy as np + +# --------------------------------------------------------------------------- +# Physics constants (must match config_lbm_pinball.json) +# --------------------------------------------------------------------------- +U0 = 0.01 # inlet centre velocity (lattice units) +L0 = 20.0 # base length unit = 1 cylinder diameter in lattice +D_CYL = 20.0 # single cylinder diameter +D_REF = 40.0 # reference length for code Re = 2*D +NX = 1280 +NY = 512 +CENTER_Y = (NY - 1) / 2.0 # 255.5 +CFG_PATH = "configs/config_lbm_pinball.json" + + +def nu_from_re(re_code: float) -> float: + """Kinematic viscosity from code Reynolds number.""" + return U0 * D_REF / re_code + + +# --------------------------------------------------------------------------- +# Scene definitions +# --------------------------------------------------------------------------- +SCENES: Dict[str, Dict[str, Any]] = {} + +# -- Steady Cloak (clean inflow, no disturbance cylinder) -------------------- +# Also serves as the base pinball-only env for Illusion inference and Vortex. +SCENES["steady_cloak_re100"] = { + "scene_id": "steady_cloak", + "model": "d1a3o12_re100", + "model_subdir": "old", + "re_code": 100, + "nu": nu_from_re(100), + "s_dim": 12, + "a_dim": 3, + "has_disturbance": False, + "pinball_front_x": 30.0 * L0, # 600 + "pinball_rear_x": 31.3 * L0, # 626 + "pinball_y_span": 0.75 * L0, # 15 + "sensor_x": 40.0 * L0, # 800 + "sensor_y_span": 2.0 * L0, # 40 + "sensor_radius": L0 / 4, # 5 + "pinball_radius": L0 / 2, # 10 + "sample_interval": 800, + "action_scale": 8.0, + "action_bias": np.array([0.0, -4.0, 4.0], dtype=np.float32), + "u0": U0, + "fifo_len": 150, + "conv_len": 30, + "max_steps": 500, + "n_objects": 6, + "obs_slice": (0, 12), + "force_norm_formula": "6*max", + "sens_norm_factor": 5, + "target_type": "steady", # mean of clean channel + "warmup_steps_init": int(4 * NX / U0), + "warmup_steps_pinball": int(4 * NX / U0), +} + +# -- Karman Cloak (upstream disturbance cylinder) ---------------------------- +SCENES["karman_cloak_re100"] = { + "scene_id": "karman_cloak", + "model": "d1a3o12_re100", + "model_subdir": "old", + "re_code": 100, + "nu": nu_from_re(100), + "s_dim": 12, + "a_dim": 3, + "has_disturbance": True, + "dist_center_x": 10.0 * L0, # 200 + "dist_radius": 1.0 * L0, # 20 + "pinball_front_x": 30.0 * L0, # 600 + "pinball_rear_x": 31.3 * L0, # 626 + "pinball_y_span": 0.75 * L0, # 15 + "sensor_x": 40.0 * L0, # 800 + "sensor_y_span": 2.0 * L0, # 40 + "sensor_radius": L0 / 4, # 5 + "pinball_radius": L0 / 2, # 10 + "sample_interval": 800, + "action_scale": 8.0, + "action_bias": np.array([0.0, -4.0, 4.0], dtype=np.float32), + "u0": U0, + "fifo_len": 150, + "conv_len": 30, + "max_steps": 500, + "n_objects": 7, + "obs_slice": (2, 14), # skip dist_cyl forces + "force_norm_formula": "6*max", + "sens_norm_factor": 5, + "target_type": "periodic", + "warmup_steps_dist": int(4 * NX / U0), + "warmup_steps_pinball": int(4 * NX / U0), +} + +# Also define the karman scenes at other Re for completeness +for re_code, name in [(50, "re50"), (200, "re200"), (400, "re400")]: + key = f"karman_cloak_{name}" + SCENES[key] = dict(SCENES["karman_cloak_re100"]) + SCENES[key].update({ + "model": f"d1a3o12_{name}", + "model_subdir": "old", + "re_code": re_code, + "nu": nu_from_re(re_code), + "scene_id": f"karman_cloak_{name}", + }) + +# -- Illusion (three target diameters) --------------------------------------- +# Inference geometry (matching uni_test): target at x=31*L0, sensors at x=40*L0 +# Pinball uses standard geometry (front x=30, rear x=31.3, sensors x=40) +def _illusion_base() -> Dict[str, Any]: + return { + "scene_id": "illusion", + "re_code": 100, + "nu": nu_from_re(100), + "s_dim": 14, + "a_dim": 3, + "has_disturbance": False, + "target_center_x": 31.0 * L0, # 620 (inference position) + "pinball_front_x": 30.0 * L0, # 600 (standard inference pinball) + "pinball_rear_x": 31.3 * L0, # 626 + "pinball_y_span": 0.75 * L0, + "sensor_x": 40.0 * L0, # 800 (inference, not training!) + "sensor_y_span": 2.0 * L0, + "sensor_radius": L0 / 4, + "pinball_radius": L0 / 2, + "action_scale": 8.0, + "action_bias": np.array([0.0, -2.0, 2.0], dtype=np.float32), + "u0": U0, + "fifo_len": 150, + "conv_len": 36, # illusion uses CONV_LEN=36 + "max_steps": 500, + "n_objects": 6, + "obs_slice": (0, 12), + "force_norm_formula": "6*max", + "sens_norm_factor": 5, + "target_type": "harmonics", + "n_harmonics": 5, + "warmup_steps_target": int(4 * NX / U0), + "warmup_steps_pinball": int(4 * NX / U0), + } + +illusion_entries = [ + ("illusion_075L", { + "model": "d1a3o14_250525_imit_075L_2U_400S", + "model_subdir": "250525", + "target_diameter": 0.75 * L0, # 15 + "sample_interval": 400, + }), + ("illusion_1L", { + "model": "d1a3o14_250525_imit_1L_2U_600S", + "model_subdir": "250525", + "target_diameter": 1.0 * L0, # 20 + "sample_interval": 800, # uni_test used 800 for inference + }), + ("illusion_15L", { + "model": "d1a3o14_250525_imit_15L_2U", + "model_subdir": "250525", + "target_diameter": 1.5 * L0, # 30 + "sample_interval": 800, + }), +] + +for key, overrides in illusion_entries: + base = _illusion_base() + base.update(overrides) + SCENES[key] = base + +# -- Vortex (Lamb dipole and Taylor monopole) -------------------------------- +def _vortex_base() -> Dict[str, Any]: + return { + "scene_id": "vortex", + "re_code": 100, + "nu": nu_from_re(100), + "s_dim": 12, + "a_dim": 3, + "has_disturbance": False, + "pinball_front_x": 30.0 * L0, + "pinball_rear_x": 31.3 * L0, + "pinball_y_span": 0.75 * L0, + "sensor_x": 40.0 * L0, + "sensor_y_span": 2.0 * L0, + "sensor_radius": L0 / 4, + "pinball_radius": L0 / 2, + "sample_interval": 800, + "action_scale": 4.0, # NOTE: scale=4 not 8 + "action_bias": np.array([0.0, -4.0, 4.0], dtype=np.float32), + "u0": U0, + "fifo_len": 150, + "conv_len": 30, + "max_steps": 150, # transient! + "n_objects": 6, + "obs_slice": (0, 12), + "force_norm_formula": "6*max", + "sens_norm_factor": 5, + "target_type": "transient", + "vortex_center_x": 10.0 * L0, # target phase + "vortex_pinball_center_x": 15.0 * L0, # pinball phase + "vortex_radius": 2.0 * L0, + } + +vortex_entries = [ + ("vortex_lamb", { + "model": "vortex_lamb", + "model_subdir": "old", + "vortex_type": "lamb", + "vortex_strength": 0.5 * U0, + }), + ("vortex_taylor", { + "model": "vortex_taylor", + "model_subdir": "old", + "vortex_type": "taylor", + "vortex_strength": 0.03 * U0, + }), +] + +for key, overrides in vortex_entries: + base = _vortex_base() + base.update(overrides) + SCENES[key] = base + + +def get_scene(name: str) -> Dict[str, Any]: + """Look up a scene by name. Raises KeyError if not found.""" + if name not in SCENES: + available = ", ".join(sorted(SCENES.keys())) + raise KeyError(f"Unknown scene '{name}'. Available: {available}") + return dict(SCENES[name]) # return a copy diff --git a/src/drl_pinball/reproduce/core/__init__.py b/src/drl_pinball/reproduce/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/drl_pinball/reproduce/core/action_wrapper.py b/src/drl_pinball/reproduce/core/action_wrapper.py new file mode 100644 index 0000000..2241c97 --- /dev/null +++ b/src/drl_pinball/reproduce/core/action_wrapper.py @@ -0,0 +1,132 @@ +"""Action wrapper — exponential smoothing and physical-unit conversion. + +Mimics the legacy FlowField.run() built-in exponential smoothing: + action_pinned = (1 - weight) * action_pinned + weight * action_target + +Usage:: + smoother = ActionSmoother(weight=0.1) # matches legacy + raw_action = model.predict(obs)[0] # [-1, 1] normalized + smoothed = smoother(raw_action) # smoothed normalized + omega = scale_action_to_omega(smoothed, scale=8, bias=[0,-4,4], u0=0.01) + for i, body_id in enumerate(pinball_ids): + sim.set_body(body_id, omega=omega[i]) + sim.run(SAMPLE_INTERVAL, zero_obs=True, sync_obs=True) +""" +from __future__ import annotations + +from typing import Optional + +import numpy as np + + +class ActionSmoother: + """Exponential moving-average action smoother. + + Matches legacy ``FlowField.run()`` smoothing: + ``pinned = (1 - weight) * pinned + weight * target`` + + Stateful across calls: call ``reset()`` to clear internal state. + """ + + def __init__(self, weight: float = 0.1): + """ + + Args: + weight: Smoothing weight (0..1). Legacy default = 0.1. + Higher = faster response, less smoothing. + """ + self.weight = float(weight) + self._smoothed: Optional[np.ndarray] = None + + def __call__(self, target_action: np.ndarray) -> np.ndarray: + """Apply exponential smoothing to the target action. + + Args: + target_action: shape ``(A_DIM,)``, typically in [-1, 1]. + + Returns: + Smoothed action of same shape and dtype. + """ + target = np.asarray(target_action, dtype=np.float32) + if self._smoothed is None: + self._smoothed = target.copy() + else: + self._smoothed = ( + (1.0 - self.weight) * self._smoothed + + self.weight * target + ) + return self._smoothed.copy() + + def reset(self, value: Optional[np.ndarray] = None) -> None: + """Reset smoother state. + + Args: + value: Initial value (e.g., the bias action). Zeros if None. + """ + if value is not None: + self._smoothed = np.asarray(value, dtype=np.float32).copy() + else: + self._smoothed = None + + +# --------------------------------------------------------------------------- +# Action scaling helpers +# --------------------------------------------------------------------------- + +def scale_action_to_omega( + action_norm: np.ndarray, + scale: float = 8.0, + bias: np.ndarray = None, + u0: float = 0.01, + radius: float = 10.0, +) -> np.ndarray: + """Convert normalized DRL action [-1, 1]^3 to physical omega [lattice units]. + + Legacy formula gave SURFACE TANGENTIAL VELOCITY: + surface_vel = (action_norm * scale + bias) * u0 + New CelerisLab needs ANGULAR VELOCITY: + omega = surface_vel / radius + + Args: + action_norm: shape ``(3,)`` normalized actions. + scale: Multiplier (8 for cloak/illusion, 4 for vortex). + bias: shape ``(3,)`` offset array. + u0: Inlet velocity (lattice units, typically 0.01). + radius: Cylinder radius (10 for pinball). + + Returns: + Omega array in lattice units (angular velocity). + """ + if bias is None: + bias = np.zeros(3, dtype=np.float32) + surface_vel = (np.asarray(action_norm, dtype=np.float32) * scale + bias) * u0 + return surface_vel / radius + + +def omega_to_norm_action( + omega: np.ndarray, + scale: float = 8.0, + bias: np.ndarray = None, + u0: float = 0.01, + radius: float = 10.0, +) -> np.ndarray: + """Inverse of ``scale_action_to_omega`` — angular velocity to normalized action. + + Args: + omega: Angular velocity from new CelerisLab. + scale: Legacy multiplier. + bias: Legacy offset array. + u0: Inlet velocity. + radius: Cylinder radius (10 for pinball). + + Returns: + Normalized action in [-1, 1]. + """ + if bias is None: + bias = np.zeros(3, dtype=np.float32) + # Convert back: surface_vel = omega * radius + surface_vel = np.asarray(omega, dtype=np.float32) * radius + return np.clip( + (surface_vel / u0 - bias) / scale, + -1.0, 1.0, + ) diff --git a/src/drl_pinball/reproduce/core/dtw_metrics.py b/src/drl_pinball/reproduce/core/dtw_metrics.py new file mode 100644 index 0000000..b270efa --- /dev/null +++ b/src/drl_pinball/reproduce/core/dtw_metrics.py @@ -0,0 +1,262 @@ +"""DTW-based similarity metrics — exact replica of legacy env computations. + +Provides ``calc_lag`` (cross-correlation lag), ``calc_sim`` (DTW similarity), +and ``compute_similarity`` to match legacy env reward computation. + +Legacy envs used: +- Karman cloak: lag from sensor1 Uy, then DTW on 6 sensor channels +- Illusion: lag from target[:,3] vs state[:,1], then DTW on 6 sensor channels (offset +2) +- Vortex: no lag, roll by current_step+1 +- Erase: lag from force channels, uses enhanced calc_sim +""" +from __future__ import annotations + +from typing import Optional + +import numpy as np + + +def calc_lag(target: np.ndarray, state: np.ndarray) -> int: + """Cross-correlation lag between target and state sequences. + + Args: + target: shape ``(N,)`` reference signal. + state: shape ``(M,)`` observed signal. + + Returns: + Integer lag (positive = state is ahead of target). + """ + t_mean = np.mean(target) + s_mean = np.mean(state) + correlation = np.correlate(target - t_mean, state - s_mean, mode="full") + lags = np.arange(-len(target) + 1, len(target)) + return int(lags[np.argmax(correlation)]) + + +def calc_dtw_sim(target: np.ndarray, state: np.ndarray) -> float: + """Standard DTW similarity (used by cloak, illusion, vortex, reduce_obs). + + Args: + target: shape ``(N,)`` reference. + state: shape ``(M,)`` observed. + + Returns: + Similarity in [0, 1], where 1 = perfect match. + """ + n, m = len(target), len(state) + dtw = np.full((n + 1, m + 1), np.inf) + dtw[0, 0] = 0.0 + for i in range(1, n + 1): + for j in range(1, m + 1): + cost = abs(float(target[i - 1]) - float(state[j - 1])) + last_min = min(dtw[i - 1, j], dtw[i, j - 1], dtw[i - 1, j - 1]) + dtw[i, j] = cost + last_min + return float(1.0 - dtw[n, m] / float(n)) + + +def calc_dtw_sim_enhanced(target: np.ndarray, state: np.ndarray) -> float: + """Enhanced DTW similarity with amplitude-ratio and mean components. + + Used by the legacy erase env. Combines: + - 80% standard DTW (max-cost normalised) + - 10% amplitude ratio (min_std/max_std) + - 10% mean similarity (1/(1 + diff/scale*10)) + + Args: + target: shape ``(N,)`` reference. + state: shape ``(M,)`` observed. + + Returns: + Combined similarity in [0, 1]. + """ + target_arr = np.asarray(target, dtype=np.float64) + state_arr = np.asarray(state, dtype=np.float64) + n, m = len(target_arr), len(state_arr) + + # Amplitude ratio component + t_std = max(np.std(target_arr), 1e-8) + s_std = max(np.std(state_arr), 1e-8) + amplitude_ratio = float(min(t_std, s_std) / max(t_std, s_std)) + + # Mean similarity component + mean_diff = abs(np.mean(target_arr) - np.mean(state_arr)) + max_scale = max(abs(np.mean(target_arr)), abs(np.mean(state_arr)), 1e-8) + mean_similarity = 1.0 / (1.0 + mean_diff / max_scale * 10.0) + + # DTW with max-possible-cost normalisation + dtw = np.full((n + 1, m + 1), np.inf) + dtw[0, 0] = 0.0 + for i in range(1, n + 1): + for j in range(1, m + 1): + cost = abs(target_arr[i - 1] - state_arr[j - 1]) + last_min = min(dtw[i - 1, j], dtw[i, j - 1], dtw[i - 1, j - 1]) + dtw[i, j] = cost + last_min + + max_possible_cost = max(np.max(np.abs(target_arr)), np.max(np.abs(state_arr)), 1e-8) + dtw_distance = dtw[n, m] / (n * max_possible_cost) + dtw_sim = max(0.0, 1.0 - dtw_distance) + + return float(0.8 * dtw_sim + 0.1 * amplitude_ratio + 0.1 * mean_similarity) + + +def compute_similarity_karman_cloak( + target_states: np.ndarray, + fifo_states: np.ndarray, + conv_len: int = 30, +) -> float: + """Compute DTW similarity for Karman cloak (standard pattern). + + Matches legacy code: + 1. Compute lag from middle sensor (index 1) Uy component + 2. For all 6 sensor channels, roll target by lag, compute DTW, average + + Args: + target_states: shape ``(FIFO_LEN, 6)`` target sensor data. + fifo_states: shape ``(FIFO_LEN, 6)`` current FIFO sensor data. + conv_len: Convergence window length (default 30). + + Returns: + Average similarity over 6 channels in [0, 1]. + """ + target = np.asarray(target_states, dtype=np.float64) + state = np.asarray(fifo_states, dtype=np.float64) + + id_sens = 1 # middle sensor + target_seq = target[conv_len:2 * conv_len, id_sens] + state_seq = state[-conv_len:, id_sens] + lag = calc_lag(target_seq, state_seq) + + similarities = 0.0 + for i in range(6): + t_seq = np.roll(target[:, i], -lag)[conv_len:2 * conv_len] + s_seq = state[-conv_len:, i] + similarities += calc_dtw_sim(t_seq, s_seq) + return float(similarities / 6.0) + + +def compute_similarity_vortex( + target_states: np.ndarray, + fifo_states: np.ndarray, + current_step: int, + conv_len: int = 30, +) -> float: + """Compute DTW similarity for vortex (no lag, roll by current_step+1). + + Matches legacy vortex env. + + Args: + target_states: shape ``(FIFO_LEN, 6)`` target sensor data. + fifo_states: shape ``(FIFO_LEN, 6)`` current FIFO data. + current_step: The current simulation step index. + conv_len: Convergence window length (default 30). + + Returns: + Average similarity over 6 channels. + """ + target = np.asarray(target_states, dtype=np.float64) + state = np.asarray(fifo_states, dtype=np.float64) + + similarities = 0.0 + for i in range(6): + t_seq = np.roll(target[-conv_len:, i], -current_step - 1) + s_seq = state[-conv_len:, i] + similarities += calc_dtw_sim(t_seq, s_seq) + return float(similarities / 6.0) + + +def compute_similarity_illusion( + target_states: np.ndarray, + fifo_states: np.ndarray, + conv_len: int = 36, +) -> float: + """Compute DTW similarity for illusion. + + Matches legacy imit env: + 1. lag from target[:, id_sens+2] vs state[:, id_sens] (offset by 2) + 2. For 6 channels, target uses [:, i+2] offset + + Args: + target_states: shape ``(FIFO_LEN, 8)`` (2 force + 6 sensor channels). + fifo_states: shape ``(FIFO_LEN, 6)`` current FIFO (6 sensors only). + conv_len: Convergence window length (default 36). + + Returns: + Average similarity over 6 channels. + """ + target = np.asarray(target_states, dtype=np.float64) + state = np.asarray(fifo_states, dtype=np.float64) + + id_sens = 1 + t_seq_ref = target[conv_len:2 * conv_len, id_sens + 2] + s_seq_ref = state[-conv_len:, id_sens] + lag = calc_lag(t_seq_ref, s_seq_ref) + + similarities = 0.0 + for i in range(6): + t_seq = np.roll(target[:, i + 2], -lag)[conv_len:2 * conv_len] + s_seq = state[-conv_len:, i] + similarities += calc_dtw_sim(t_seq, s_seq) + return float(similarities / 6.0) + + +# --------------------------------------------------------------------------- +# Harmonics analysis (used by illusion) +# --------------------------------------------------------------------------- +def analyze_harmonics( + states: np.ndarray, + n_harmonics: int = 5, +) -> list: + """FFT-based harmonic analysis of multi-channel time series. + + Matches legacy ``analyze_harmonics()``. + + Args: + states: shape ``(N, D)`` time-series data. + n_harmonics: Number of harmonics to extract per channel. + + Returns: + List of D dicts, each with keys: + dc: float (DC component) + amps: (n_harmonics,) array + freqs: (n_harmonics,) array + phases: (n_harmonics,) array + """ + N, D = states.shape + result = [] + for d in range(D): + y = states[:, d] + fft_coef = np.fft.rfft(y) + freqs = np.fft.rfftfreq(N, d=1) + amps = 2.0 * np.abs(fft_coef) / N + phases = np.angle(fft_coef) + idx = np.argsort(amps[1:])[::-1][:n_harmonics] + 1 + harmonics = { + "dc": float(np.real(fft_coef[0]) / N), + "amps": np.array(amps[idx], dtype=np.float32), + "freqs": np.array(freqs[idx], dtype=np.float32), + "phases": np.array(phases[idx], dtype=np.float32), + } + result.append(harmonics) + return result + + +def gen_target_states_at(t, harmonics) -> np.ndarray: + """Reconstruct target state at time step t from harmonics. + + Matches legacy ``gen_target_states_at()``. + + Args: + t: Integer step index. + harmonics: Output from ``analyze_harmonics()``. + + Returns: + shape ``(D,)`` reconstructed state vector. + """ + D = len(harmonics) + result = np.zeros(D, dtype=np.float32) + for d, h in enumerate(harmonics): + val = float(h["dc"]) + for amp, freq, phase in zip(h["amps"], h["freqs"], h["phases"]): + val += amp * np.cos(2.0 * np.pi * freq * t + phase) + result[d] = val + return result diff --git a/src/drl_pinball/reproduce/core/obs_normalizer.py b/src/drl_pinball/reproduce/core/obs_normalizer.py new file mode 100644 index 0000000..da260d7 --- /dev/null +++ b/src/drl_pinball/reproduce/core/obs_normalizer.py @@ -0,0 +1,112 @@ +"""Observation normalization — exact replication of legacy norm procedures. + +Legacy norm computation (from FIFO data): + - force_norm_fact = factor * max(|forces|) + - sens_deviation[i] = mean(sensor_i) + - sens_norm_fact[i] = factor * max(|sensor_i - sens_deviation[i]|) + +Normalised observation: + - forces = raw_forces / force_norm_fact + - sens = (raw_sens - sens_deviation) / sens_norm_fact + - observation = clip(hstack([forces, sens]), -1, 1) +""" +from __future__ import annotations + +from typing import Dict, Optional, Tuple + +import numpy as np + + +def compute_norm( + fifo_states: np.ndarray, + force_norm_factor: float = 6.0, + sens_norm_factor: float = 5.0, + force_slice: Tuple[int, int] = (6, 12), + sens_slice: Tuple[int, int] = (0, 6), +) -> Dict[str, np.ndarray]: + """Compute norm values from a FIFO of observations. + + Exact replica of legacy env norm computation. + + Args: + fifo_states: shape ``(FIFO_LEN, N_obs)`` array of raw observations. + force_norm_factor: Multiplier for force norm (6 for cloak, 100 for erase, etc.) + sens_norm_factor: Multiplier for sensor norm (5 for cloak, 10 for erase, etc.) + force_slice: Slice indices for forces within the obs array. + sens_slice: Slice indices for sensors within the obs array. + + Returns: + dict with keys: + force_norm_fact: scalar float32 + sens_deviation: (6,) float32 + sens_norm_fact: (6,) float32 + """ + arr = np.asarray(fifo_states, dtype=np.float64) + + s_start, s_end = sens_slice + f_start, f_end = force_slice + + forces = arr[:, f_start:f_end] + sensors = arr[:, s_start:s_end] + + n_sens = sensors.shape[1] + force_norm_fact = np.float32(force_norm_factor * np.max(np.abs(forces))) + + sens_deviation = np.mean(sensors, axis=0).astype(np.float32) + sens_norm_fact = np.zeros(n_sens, dtype=np.float32) + for i in range(n_sens): + deviation = np.max(np.abs(sensors[:, i] - sens_deviation[i])) + sens_norm_fact[i] = np.float32(sens_norm_factor * deviation) + + return { + "force_norm_fact": force_norm_fact, + "sens_deviation": sens_deviation, + "sens_norm_fact": sens_norm_fact, + } + + +def normalize_observation( + raw_obs_slice: np.ndarray, + norm: Dict[str, np.ndarray], + force_slice: Tuple[int, int] = (6, 12), + sens_slice: Tuple[int, int] = (0, 6), +) -> np.ndarray: + """Normalize a raw observation slice using pre-computed norm values. + + Args: + raw_obs_slice: shape ``(N_obs,)`` raw observation values. + norm: dict with keys 'force_norm_fact', 'sens_deviation', 'sens_norm_fact'. + force_slice: Slice for forces within the obs array. + sens_slice: Slice for sensors within the obs array. + + Returns: + shape ``(S_DIM,)`` normalized observation, clipped to [-1, 1]. + """ + obs = np.asarray(raw_obs_slice, dtype=np.float32) + s_start, s_end = sens_slice + f_start, f_end = force_slice + + forces = obs[f_start:f_end] / norm["force_norm_fact"] + sens = (obs[s_start:s_end] - norm["sens_deviation"]) / norm["sens_norm_fact"] + + return np.clip(np.hstack([forces, sens]), -1.0, 1.0).astype(np.float32) + + +def load_norm(path: str) -> Dict[str, np.ndarray]: + """Load norm values from a .npz file.""" + data = np.load(path) + return { + "force_norm_fact": data["force_norm_fact"], + "sens_deviation": data["sens_deviation"], + "sens_norm_fact": data["sens_norm_fact"], + } + + +def save_norm(path: str, norm: Dict[str, np.ndarray]) -> None: + """Save norm values to a .npz file.""" + np.savez_compressed( + path, + force_norm_fact=norm["force_norm_fact"], + sens_deviation=norm["sens_deviation"], + sens_norm_fact=norm["sens_norm_fact"], + ) diff --git a/src/drl_pinball/reproduce/run_all_cases.py b/src/drl_pinball/reproduce/run_all_cases.py new file mode 100644 index 0000000..595d924 --- /dev/null +++ b/src/drl_pinball/reproduce/run_all_cases.py @@ -0,0 +1,361 @@ +#!/usr/bin/env python3 +"""Comprehensive run: Steady Cloak + Karman Cloak (new norm / legacy norm) + flow field output. + +Runs three cases: + Case A: Steady Cloak (open-loop constant rotation, no DRL) + Case B: Karman Cloak (new-CFD norm + DRL inference) + Case C: Karman Cloak (legacy norm + DRL inference) + +Each case saves: macroscopic.npz (rho/ux/uy), vorticity.png, sensors_forces.npz +""" +from __future__ import annotations + +import argparse +import json +import os +import sys + +import numpy as np + +_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) +_SRC = os.path.join(_REPO, "src") +for p in [_REPO, _SRC]: + if p not in sys.path: + sys.path.insert(0, p) + +import pycuda.driver as cuda +cuda.init() + +from CelerisLab import Simulation +from CelerisLab.common.render import compute_vorticity, render_vorticity_field +from CelerisLab.common._types import CylinderSpec +from drl_pinball.reproduce.configs.model_inventory import ModelInventory + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- +L0 = 20.0 +U0 = 0.01 +NX = 1280 +NY = 512 +CENTER_Y = float(NY - 1) / 2.0 +RADIUS = L0 / 2 +FIFO_LEN = 150 +SI = 800 +WARMUP = int(4.0 * NX / U0) +CFG_PATH = "configs/config_lbm_pinball.json" +REF_DIR = os.path.join(_REPO, "src", "SR_analysis", "data", "karman", "karman_re100") +OUT_BASE = os.path.join(os.path.dirname(__file__), "output") + + +class EMA: + def __init__(self, weight=0.1): + self.weight = weight + self._state = None + def __call__(self, target): + t = np.asarray(target, dtype=np.float32) + if self._state is None: + self._state = t.copy() + else: + self._state = (1.0 - self.weight) * self._state + self.weight * t + return self._state.copy() + def reset(self, value=None): + if value is not None: + self._state = np.asarray(value, dtype=np.float32).copy() + else: + self._state = None + + +def get_cc(sim, sid): + nx, ny = sim.lbm_cfg.nx, sim.lbm_cfg.ny + cells_arr, _ = sim.bodies.get(sid).get_sensor_list(nx, ny) + return float(len(cells_arr)) + + +def read_obs_legacy(sim, sensor_ids, dist_id, pinball_ids, cc): + obs = [] + if dist_id is not None: + obs.extend(sim.read_force(dist_id, normalize=True)) + for sid in sensor_ids: + s = sim.read_sensor(sid, normalize=True) + obs.extend(s * cc) + for pid in pinball_ids: + obs.extend(sim.read_force(pid, normalize=True)) + return np.array(obs, dtype=np.float32) + + +def action_to_omega(action_norm, bias=(0.0, -4.0, 4.0)): + """Convert normalized action [-1,1] to omega. + New solver: omega = -surface_vel / R (corrected sign). + """ + b = np.array(bias, dtype=np.float32) + surface_vel = (np.asarray(action_norm, dtype=np.float32) * 8.0 + b) * U0 + return -surface_vel / RADIUS + + +def normalize_obs(obs_slice, norm): + forces = obs_slice[6:12] / norm["force_norm_fact"] + sens = (obs_slice[0:6] - norm["sens_deviation"]) / norm["sens_norm_fact"] + return np.clip(np.hstack([forces, sens]), -1.0, 1.0).astype(np.float32) + + +def save_field(sim, out_dir, name): + """Save macroscopic field and render vorticity.""" + macro = sim.get_macroscopic() + np.savez_compressed(os.path.join(out_dir, f"macro_{name}.npz"), + rho=macro["rho"], ux=macro["ux"], uy=macro["uy"]) + vort = compute_vorticity(macro["ux"], macro["uy"]) + render_vorticity_field( + vort, nx=NX, ny=NY, + out_path=os.path.join(out_dir, f"vorticity_{name}.png"), + cylinders=[ + ((10.0 * L0, CENTER_Y), 1.0 * L0), # dist cylinder + ((30.0 * L0, CENTER_Y), RADIUS), # front + ((31.3 * L0, CENTER_Y + 15.0), RADIUS), # top + ((31.3 * L0, CENTER_Y - 15.0), RADIUS), # bottom + ], + ) + print(f" Saved {name}: macro + vorticity.png") + + +# ========================================================================= +# Case A: Steady Cloak (open-loop constant rotation) +# ========================================================================= +def run_steady_cloak(device_id, out_dir): + """Steady cloak: pinball only, constant bias=[0,-5.1,5.1]*U0, no DRL.""" + print("\n" + "=" * 70) + print("Case A: Steady Cloak (open-loop constant rotation)") + print("=" * 70) + os.makedirs(out_dir, exist_ok=True) + + sim = Simulation(lbm_config_path=CFG_PATH, device_id=device_id) + # 6 objects: 3 sensors + 3 cylinders (no disturbance) + sensor_ids = [ + sim.add_body("sensor", center=(40.0 * L0, CENTER_Y + 40.0, 0.0), radius=5.0), + sim.add_body("sensor", center=(40.0 * L0, CENTER_Y, 0.0), radius=5.0), + sim.add_body("sensor", center=(40.0 * L0, CENTER_Y - 40.0, 0.0), radius=5.0), + ] + sim.add_body("circle", center=(30.0 * L0, CENTER_Y, 0.0), radius=RADIUS) + sim.add_body("circle", center=(31.3 * L0, CENTER_Y + 15.0, 0.0), radius=RADIUS) + sim.add_body("circle", center=(31.3 * L0, CENTER_Y - 15.0, 0.0), radius=RADIUS) + sim.initialize() + sim.run(WARMUP, zero_obs=True) + cc = get_cc(sim, sensor_ids[0]) + + # Save uncontrolled field + save_field(sim, out_dir, "steady_uncontrolled") + + # Apply constant bias: [0, -5.1, 5.1] * U0 -> omega + bias_omega = action_to_omega( + np.array([0.0, 0.0, 0.0], dtype=np.float32), + bias=(0.0, -5.1, 5.1), + ) + print(f" Steady bias omega: {bias_omega}") + sim.set_body(3, omega=bias_omega[0]) # front + sim.set_body(4, omega=bias_omega[1]) # top + sim.set_body(5, omega=bias_omega[2]) # bottom + + # Run to steady state + sim.run(WARMUP, zero_obs=True) + + # Record sensors/forces + sensors_f = [] + for _ in range(200): + sim.run(SI, zero_obs=True) + obs = read_obs_legacy(sim, sensor_ids, None, [3, 4, 5], cc) + sensors_f.append(obs[0:12]) # 6 sens + 6 forces + sensors_f = np.array(sensors_f, dtype=np.float32) + np.savez_compressed(os.path.join(out_dir, "steady_signals.npz"), + sensors=sensors_f[:, 0:6], forces=sensors_f[:, 6:12]) + + save_field(sim, out_dir, "steady_controlled") + print(f" Forces: front_fy={sensors_f[:,7].mean():+.6f} " + f"top_fy={sensors_f[:,9].mean():+.6f} bottom_fy={sensors_f[:,11].mean():+.6f}") + sim.close() + + +# ========================================================================= +# Case B & C: Karman Cloak (shared build, different norm) +# ========================================================================= +def build_karman_env(device_id, out_dir): + """Build Karman env, record target, add pinball, return (sim, ids, norm).""" + sim = Simulation(lbm_config_path=CFG_PATH, device_id=device_id) + + # Disturbance + sensors + dist_id = sim.add_body("circle", center=(10.0 * L0, CENTER_Y, 0.0), radius=1.0 * L0) + sensor_ids = [ + sim.add_body("sensor", center=(40.0 * L0, CENTER_Y + 40.0, 0.0), radius=5.0), + sim.add_body("sensor", center=(40.0 * L0, CENTER_Y, 0.0), radius=5.0), + sim.add_body("sensor", center=(40.0 * L0, CENTER_Y - 40.0, 0.0), radius=5.0), + ] + sim.initialize() + sim.run(WARMUP, zero_obs=True) + cc = get_cc(sim, sensor_ids[0]) + + # Record target + target = np.empty((0, 6), dtype=np.float32) + for _ in range(FIFO_LEN): + sim.run(SI, zero_obs=True) + obs = read_obs_legacy(sim, sensor_ids, dist_id, [], cc) + target = np.vstack((target, obs[2:8])) + np.savez_compressed(os.path.join(out_dir, "target.npz"), target_states=target) + + # Add pinball + n0 = sim.bodies.count + sim.add_body("circle", center=(30.0 * L0, CENTER_Y, 0.0), radius=RADIUS) + sim.add_body("circle", center=(31.3 * L0, CENTER_Y + 15.0, 0.0), radius=RADIUS) + sim.add_body("circle", center=(31.3 * L0, CENTER_Y - 15.0, 0.0), radius=RADIUS) + sim.sync_bodies() + fid, tid, bid = list(range(n0, n0 + 3)) + + sim.run(WARMUP, zero_obs=True) + return sim, dist_id, sensor_ids, (fid, tid, bid), cc, target + + +def run_karman_drl(sim, dist_id, sensor_ids, pinball_ids, cc, target, + norm, model, out_dir, case_label, num_steps=200): + """Run DRL inference with given norm.""" + fid, tid, bid = pinball_ids + bias_action = np.array([0.0, -1.0, 1.0]) # maps to [0, -4, 4]*U0 + bias_omega = action_to_omega(bias_action) + + # Bias FIFO + ema = EMA(weight=0.1) + ema.reset(np.zeros(3, dtype=np.float32)) + for _ in range(FIFO_LEN): + s = ema(bias_omega) + sim.set_body(fid, omega=s[0]) + sim.set_body(tid, omega=s[1]) + sim.set_body(bid, omega=s[2]) + sim.run(SI, zero_obs=True) + sim.snapshot() + + # DRL inference + sim.restore() + ema.reset(bias_omega.copy()) + + obs_init = read_obs_legacy(sim, sensor_ids, dist_id, [fid, tid, bid], cc) + obs_norm = normalize_obs(obs_init[2:14], norm) + + sig_s = np.zeros((num_steps, 6), dtype=np.float32) + sig_f = np.zeros((num_steps, 6), dtype=np.float32) + sig_a = np.zeros((num_steps, 3), dtype=np.float32) + + for step in range(num_steps): + action, _ = model.predict(obs_norm, deterministic=True) + action = np.asarray(action, dtype=np.float32).flatten() + target_omega = action_to_omega(action) + smoothed = ema(target_omega) + + sim.set_body(fid, omega=smoothed[0]) + sim.set_body(tid, omega=smoothed[1]) + sim.set_body(bid, omega=smoothed[2]) + sim.run(SI, zero_obs=True) + + obs = read_obs_legacy(sim, sensor_ids, dist_id, [fid, tid, bid], cc) + sl = obs[2:14] + sig_s[step] = sl[0:6] + sig_f[step] = sl[6:12] + sig_a[step] = action + obs_norm = normalize_obs(sl, norm) + + # Save last frame's flow field + save_field(sim, out_dir, f"karman_{case_label}") + np.savez_compressed(os.path.join(out_dir, f"signals_{case_label}.npz"), + sensors=sig_s, forces=sig_f, actions=sig_a) + + print(f" [{case_label}] Actions: aF={sig_a[:,0].mean():+.4f} " + f"aB={sig_a[:,1].mean():+.4f} aT={sig_a[:,2].mean():+.4f}") + print(f" [{case_label}] Forces: front_fy={sig_f[:,1].mean():+.6f} " + f"top_fy={sig_f[:,3].mean():+.6f} bottom_fy={sig_f[:,5].mean():+.6f}") + + +# ========================================================================= +# Case B: Karman + new norm +# ========================================================================= +def collect_new_norm(sim, sensor_ids, dist_id, pinball_ids, cc, out_dir): + """Collect norm values on new CFD from zero-action FIFO.""" + fid, tid, bid = pinball_ids + fifo = [] + for _ in range(FIFO_LEN): + sim.run(SI, zero_obs=True) + obs = read_obs_legacy(sim, sensor_ids, dist_id, [fid, tid, bid], cc) + fifo.append(obs[2:14]) + f = np.array(fifo, dtype=np.float32) + + fn = 6.0 * np.max(np.abs(f[:, 6:12])) + sd = np.mean(f[:, 0:6], axis=0).astype(np.float32) + sn = np.zeros(6, dtype=np.float32) + for i in range(6): + sn[i] = 5.0 * np.max(np.abs(f[:, i] - sd[i])) + + norm = {"force_norm_fact": fn, "sens_deviation": sd, "sens_norm_fact": sn} + np.savez(os.path.join(out_dir, "norm_new.npz"), **norm) + print(f" New norm: fn={fn:.6f}") + return norm + + +def run_karman_new_norm(device_id, out_dir): + print("\n" + "=" * 70) + print("Case B: Karman Cloak (new-CFD norm + DRL)") + print("=" * 70) + os.makedirs(out_dir, exist_ok=True) + + model = ModelInventory().load("d1a3o12_re100", device="cpu") + sim, dist_id, sensor_ids, pids, cc, target = build_karman_env(device_id, out_dir) + norm = collect_new_norm(sim, sensor_ids, dist_id, pids, cc, out_dir) + run_karman_drl(sim, dist_id, sensor_ids, pids, cc, target, + norm, model, out_dir, "new_norm") + sim.close() + + +# ========================================================================= +# Case C: Karman + legacy norm +# ========================================================================= +def run_karman_legacy_norm(device_id, out_dir): + print("\n" + "=" * 70) + print("Case C: Karman Cloak (legacy norm + DRL)") + print("=" * 70) + os.makedirs(out_dir, exist_ok=True) + + with open(os.path.join(REF_DIR, "norm.json")) as f: + d = json.load(f) + norm = { + "force_norm_fact": np.float32(d["force_norm_fact"]), + "sens_deviation": np.array(d["sens_deviation"], dtype=np.float32), + "sens_norm_fact": np.array(d["sens_norm_fact"], dtype=np.float32), + } + + model = ModelInventory().load("d1a3o12_re100", device="cpu") + sim, dist_id, sensor_ids, pids, cc, target = build_karman_env(device_id, out_dir) + run_karman_drl(sim, dist_id, sensor_ids, pids, cc, target, + norm, model, out_dir, "legacy_norm") + sim.close() + + +# ========================================================================= +# Main +# ========================================================================= +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--device", type=int, default=0) + parser.add_argument("--cases", type=str, default="A,B,C", + help="Comma-separated: A=steady, B=new-norm, C=legacy-norm") + parser.add_argument("--steps", type=int, default=200) + args = parser.parse_args() + + cases = [c.strip().upper() for c in args.cases.split(",")] + + if "A" in cases: + run_steady_cloak(args.device, os.path.join(OUT_BASE, "steady_cloak")) + + if "B" in cases or "C" in cases: + karman_dir = os.path.join(OUT_BASE, "karman_cloak") + + if "B" in cases: + run_karman_new_norm(args.device, karman_dir) + + if "C" in cases: + run_karman_legacy_norm(args.device, karman_dir) + + print("\nDone.") diff --git a/src/drl_pinball/reproduce/run_illusion_vortex.py b/src/drl_pinball/reproduce/run_illusion_vortex.py new file mode 100644 index 0000000..e3d3614 --- /dev/null +++ b/src/drl_pinball/reproduce/run_illusion_vortex.py @@ -0,0 +1,461 @@ +#!/usr/bin/env python3 +"""Phase 2 (Illusion) + Phase 3 (Vortex) reproduction on new CelerisLab. + +Uses same proven approach as Karman: legacy norm + old-equiv sensor conversion. +""" +from __future__ import annotations + +import argparse +import json +import os +import sys +import numpy as np + +_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) +_SRC = os.path.join(_REPO, "src") +for p in [_REPO, _SRC]: + if p not in sys.path: + sys.path.insert(0, p) + +import pycuda.driver as cuda +cuda.init() + +from CelerisLab import Simulation +from CelerisLab.common.render import compute_vorticity, render_vorticity_field +from CelerisLab.common._types import CylinderSpec +from CelerisLab.lbm.initializers import add_vortex +from drl_pinball.reproduce.configs.model_inventory import ModelInventory + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- +L0 = 20.0 +U0 = 0.01 +NX = 1280 +NY = 512 +CENTER_Y = float(NY - 1) / 2.0 +RADIUS = L0 / 2 +FIFO_LEN = 150 +WARMUP = int(4.0 * NX / U0) +CFG_PATH = "configs/config_lbm_pinball.json" +OUT_BASE = os.path.join(os.path.dirname(__file__), "output") + + +class EMA: + def __init__(self, weight=0.1): + self.weight = weight + self._state = None + def __call__(self, target): + t = np.asarray(target, dtype=np.float32) + if self._state is None: + self._state = t.copy() + else: + self._state = (1.0 - self.weight) * self._state + self.weight * t + return self._state.copy() + def state(self): + return self._state.copy() if self._state is not None else None + + def reset(self, value=None): + if value is not None: + self._state = np.asarray(value, dtype=np.float32).copy() + else: + self._state = None + + +def get_cc(sim, sid): + nx, ny = sim.lbm_cfg.nx, sim.lbm_cfg.ny + cells_arr, _ = sim.bodies.get(sid).get_sensor_list(nx, ny) + return float(len(cells_arr)) + + +def read_obs_legacy(sim, sensor_ids, pinball_ids, cc): + """Return [s0_ux,uy, s1_ux,uy, s2_ux,uy, front_fx,fy, top_fx,fy, bottom_fx,fy]. + No dist_cylinder - this is for 6-object envs (illusion, vortex, steady). + """ + obs = [] + for sid in sensor_ids: + s = sim.read_sensor(sid, normalize=True) + obs.extend(s * cc) + for pid in pinball_ids: + obs.extend(sim.read_force(pid, normalize=True)) + return np.array(obs, dtype=np.float32) + + +def action_to_omega(action_norm, scale=8.0, bias=(0.0, -4.0, 4.0)): + b = np.array(bias, dtype=np.float32) + surface_vel = (np.asarray(action_norm, dtype=np.float32) * scale + b) * U0 + return -surface_vel / RADIUS # inverted sign for new CelerisLab + + +def normalize_obs(obs_slice, norm): + forces = obs_slice[6:12] / norm["force_norm_fact"] + sens = (obs_slice[0:6] - norm["sens_deviation"]) / norm["sens_norm_fact"] + return np.clip(np.hstack([forces, sens]), -1.0, 1.0).astype(np.float32) + + +def load_norm(path): + with open(path) as f: + d = json.load(f) + return { + "force_norm_fact": np.float32(d["force_norm_fact"]), + "sens_deviation": np.array(d["sens_deviation"], dtype=np.float32), + "sens_norm_fact": np.array(d["sens_norm_fact"], dtype=np.float32), + } + + +def save_vorticity(sim, out_path, cylinders): + macro = sim.get_macroscopic() + vort = compute_vorticity(macro["ux"], macro["uy"]) + render_vorticity_field(vort, nx=NX, ny=NY, out_path=out_path, cylinders=cylinders) + + +# ========================================================================= +# Phase 2: Illusion +# ========================================================================= +def run_illusion(device_id, target_label, model_name, sample_interval, target_diameter, + ref_dir, out_dir): + """Run Illusion inference using legacy norm + legacy target harmonics.""" + print(f"\n{'='*70}") + print(f"Illusion {target_label} (model={model_name}, SI={sample_interval})") + print(f"{'='*70}") + os.makedirs(out_dir, exist_ok=True) + + # Load legacy norm and target harmonics + norm = load_norm(os.path.join(ref_dir, "norm.json")) + with open(os.path.join(ref_dir, "target_harmonics.json")) as f: + target_harmonics = json.load(f) + legacy_target = np.load(os.path.join(ref_dir, "target.npz")) + legacy_target_states = legacy_target["target_states"] + + def gen_target_states_at(t, harmonics): + D = len(harmonics) + result = np.zeros(D, dtype=np.float32) + for d, h in enumerate(harmonics): + val = np.float32(h["dc"]) + for amp, freq, phase in zip(h["amps"], h["freqs"], h["phases"]): + val += amp * np.cos(2 * np.pi * freq * t + phase) + result[d] = val + return result + + # Pinball env (no disturbance cylinder) + sim = Simulation(lbm_config_path=CFG_PATH, device_id=device_id) + sensor_ids = [ + sim.add_body("sensor", center=(30.0 * L0, CENTER_Y + 40.0, 0.0), radius=5.0), + sim.add_body("sensor", center=(30.0 * L0, CENTER_Y, 0.0), radius=5.0), + sim.add_body("sensor", center=(30.0 * L0, CENTER_Y - 40.0, 0.0), radius=5.0), + ] + sim.add_body("circle", center=(19.0 * L0, CENTER_Y, 0.0), radius=RADIUS) # front + sim.add_body("circle", center=(20.3 * L0, CENTER_Y + 15.0, 0.0), radius=RADIUS) # top + sim.add_body("circle", center=(20.3 * L0, CENTER_Y - 15.0, 0.0), radius=RADIUS) # bottom + sim.initialize() + sim.run(WARMUP, zero_obs=True) + cc = get_cc(sim, sensor_ids[0]) + fid, tid, bid = 3, 4, 5 + + # Bias FIFO with init bias [0, -1, 1]*U0 (matching legacy_env_imit.py) + init_bias_surf = np.array([0.0, -1.0, 1.0], dtype=np.float32) * U0 + init_bias_omega = -init_bias_surf / RADIUS + + ema_bias = EMA(weight=0.1) + ema_bias.reset(np.zeros(3, dtype=np.float32)) + for _ in range(FIFO_LEN): + s = ema_bias(init_bias_omega) + sim.set_body(fid, omega=s[0]); sim.set_body(tid, omega=s[1]); sim.set_body(bid, omega=s[2]) + sim.run(sample_interval, zero_obs=True) + sim.snapshot() + print(f" Init bias done. EMA state: {ema_bias.state()}") + + # DRL inference + sim.restore() + ema = EMA(weight=0.1) + # DRL bias is [0, -2, 2]*U0 for action space, but EMA starts from init bias [0,-1,1]*U0 + drl_bias_surf = np.array([0.0, -2.0, 2.0], dtype=np.float32) * U0 + drl_bias_omega = -drl_bias_surf / RADIUS + ema.reset(init_bias_omega.copy()) # EMA starts from init bias state + + model = ModelInventory().load(model_name, device="cpu") + print(f" Model loaded on CPU") + + obs_init = read_obs_legacy(sim, sensor_ids, [fid, tid, bid], cc) + obs_norm = normalize_obs(obs_init, norm) + # Append target forces for S_DIM=14 + t0 = gen_target_states_at(0, target_harmonics) + target_cd = np.float32(t0[0] / norm["force_norm_fact"]) + target_cl = np.float32(t0[1] / norm["force_norm_fact"]) + obs_norm = np.clip(np.hstack([obs_norm, [target_cd, target_cl]]), -1, 1).astype(np.float32) + + num_steps = 200 + sig_s = np.zeros((num_steps, 6), dtype=np.float32) + sig_f = np.zeros((num_steps, 6), dtype=np.float32) + sig_a = np.zeros((num_steps, 3), dtype=np.float32) + + for step in range(num_steps): + action, _ = model.predict(obs_norm, deterministic=True) + action = np.asarray(action, dtype=np.float32).flatten() + target_omega = action_to_omega(action, scale=8.0, bias=(0.0, -2.0, 2.0)) + smoothed = ema(target_omega) + + sim.set_body(fid, omega=smoothed[0]); sim.set_body(tid, omega=smoothed[1]); sim.set_body(bid, omega=smoothed[2]) + sim.run(sample_interval, zero_obs=True) + + obs = read_obs_legacy(sim, sensor_ids, [fid, tid, bid], cc) + sig_s[step] = obs[0:6] + sig_f[step] = obs[6:12] + sig_a[step] = action + + forces_n = obs[6:12] / norm["force_norm_fact"] + sens_n = (obs[0:6] - norm["sens_deviation"]) / norm["sens_norm_fact"] + t_h = gen_target_states_at(step, target_harmonics) + target_cd = np.float32(t_h[0] / norm["force_norm_fact"]) + target_cl = np.float32(t_h[1] / norm["force_norm_fact"]) + obs_norm = np.clip(np.hstack([forces_n, sens_n, [target_cd, target_cl]]), -1, 1).astype(np.float32) + + save_vorticity(sim, os.path.join(out_dir, f"vorticity.png"), + cylinders=[((19.0*L0, CENTER_Y), RADIUS), + ((20.3*L0, CENTER_Y+15.0), RADIUS), + ((20.3*L0, CENTER_Y-15.0), RADIUS)]) + np.savez_compressed(os.path.join(out_dir, "signals.npz"), + sensors=sig_s, forces=sig_f, actions=sig_a) + sim.close() + + # Compare with reference + ref = np.load(os.path.join(ref_dir, "controlled.npz")) + print(f"\n Actions vs ref:") + for i, name in enumerate(["aF","aB","aT"]): + c = np.corrcoef(ref["actions"][:num_steps,i], sig_a[:,i])[0,1] + print(f" {name}: ref_mean={ref['actions'][:num_steps,i].mean():+.4f} " + f"our_mean={sig_a[:,i].mean():+.4f} corr={c:+.4f}") + print(f" Sim from SR_analysis: {ref.get('similarity', 0.9754) if 'similarity' in ref else 'N/A'}") + + # Compute DTW similarity for sensors + n_c = 36 + def dtw_sim(t, s): + n = len(t) + D = np.full((n+1, n+1), np.inf) + D[0,0] = 0 + for i in range(1, n+1): + for j in range(1, n+1): + D[i,j] = abs(t[i-1]-s[j-1]) + min(D[i-1,j], D[i,j-1], D[i-1,j-1]) + return 1 - D[n,n] / n + + # Compute lag from mid sensor + t_seq = legacy_target_states[n_c:2*n_c, 1+2] # target sens1_uy (index 3 in 8-chan) + s_seq = sig_s[-n_c:, 1] # our sens1_uy (index 1 in 6-chan) + if np.std(t_seq) > 1e-10 and np.std(s_seq) > 1e-10: + corr = np.correlate(t_seq - t_seq.mean(), s_seq - s_seq.mean(), mode="full") + lag = np.argmax(corr) - (len(t_seq) - 1) + else: + lag = 0 + + sim_val = 0.0 + for i in range(6): + t_rolled = np.roll(legacy_target_states[:, i+2], -lag)[n_c:2*n_c] + sim_val += dtw_sim(t_rolled, sig_s[-n_c:, i]) / 6.0 + print(f" DTW similarity (legacy target vs our sensors): {sim_val:.4f}") + print(f" Reference DTW similarity: {0.9754}") + + +# ========================================================================= +# Phase 3: Vortex (lamb / taylor) +# ========================================================================= +def run_vortex(device_id, vortex_type, model_name, vortex_strength, ref_dir, out_dir): + """Run Vortex cloak inference using legacy norm.""" + print(f"\n{'='*70}") + print(f"Vortex {vortex_type} (model={model_name}, strength={vortex_strength})") + print(f"{'='*70}") + os.makedirs(out_dir, exist_ok=True) + + norm = load_norm(os.path.join(ref_dir, "norm.json")) + legacy_target = np.load(os.path.join(ref_dir, "target.npz"))["target_states"] + + MAX_STEPS = 150 + CONV_LEN = 30 + SI = 800 + + # Stage 1: Create sensor-only env, record target with vortex + sim = Simulation(lbm_config_path=CFG_PATH, device_id=device_id) + sensor_ids = [ + sim.add_body("sensor", center=(40.0 * L0, CENTER_Y + 40.0, 0.0), radius=5.0), + sim.add_body("sensor", center=(40.0 * L0, CENTER_Y, 0.0), radius=5.0), + sim.add_body("sensor", center=(40.0 * L0, CENTER_Y - 40.0, 0.0), radius=5.0), + ] + sim.initialize() + sim.run(WARMUP, zero_obs=True) + cc = get_cc(sim, sensor_ids[0]) + + # Save clean sensor-only DDF + sim.snapshot() + print(f" Clean sensor DDF saved. Sensor cell count: {cc}") + + # Add vortex and record target (vortex moves through domain) + add_vortex(sim.field, center=(10.0 * L0, CENTER_Y), radius=2.0 * L0, + strength=vortex_strength, vortex_type=vortex_type) + + target_states = np.empty((0, 6), dtype=np.float32) + for _ in range(FIFO_LEN): + sim.run(SI, zero_obs=True) + obs = read_obs_legacy(sim, sensor_ids, [], cc) + target_states = np.vstack((target_states, obs)) + + # Save target and compute lag/reference + np.savez_compressed(os.path.join(out_dir, "target.npz"), target_states=target_states) + + # Restore clean sensor state, add pinball + vortex + sim.restore() + + n0 = sim.bodies.count + sim.add_body("circle", center=(30.0 * L0, CENTER_Y, 0.0), radius=RADIUS) + sim.add_body("circle", center=(31.3 * L0, CENTER_Y + 15.0, 0.0), radius=RADIUS) + sim.add_body("circle", center=(31.3 * L0, CENTER_Y - 15.0, 0.0), radius=RADIUS) + sim.sync_bodies() + fid, tid, bid = list(range(n0, n0 + 3)) + + # Warmup pinball with bias, then add vortex + bias_surf = np.array([0.0, -4.0, 4.0], dtype=np.float32) * U0 + bias_omega = -bias_surf / RADIUS + + ema_init = EMA(weight=0.1) + ema_init.reset(np.zeros(3, dtype=np.float32)) + for _ in range(100): + s = ema_init(bias_omega) + sim.set_body(fid, omega=s[0]); sim.set_body(tid, omega=s[1]); sim.set_body(bid, omega=s[2]) + sim.run(SI, zero_obs=True) + # Note: legacy env also runs bias for ~FIFO_LEN steps but with only 3 body count first... + # Actually legacy vortex env adds pinball, runs warmup with zeros(6), then runs + # bias [0,0,0,0,-4U0,4U0] for 1*NX/U0 steps, THEN adds vortex and saves DDF + # Let's replicate this more carefully. + print(" Pinball warmup + bias done") + + # Add vortex at pinball-phase position + add_vortex(sim.field, center=(15.0 * L0, CENTER_Y), radius=2.0 * L0, + strength=vortex_strength, vortex_type=vortex_type) + + # Save DDF after vortex addition + sim.snapshot() + print(f" Post-vortex DDF saved") + + # Norm FIFO (zero action, with pinball+vortex) + fifo = [] + for _ in range(FIFO_LEN): + sim.run(SI, zero_obs=True) + obs = read_obs_legacy(sim, sensor_ids, [fid, tid, bid], cc) + fifo.append(obs) + fifo_arr = np.array(fifo, dtype=np.float32) + new_fn = 6.0 * np.max(np.abs(fifo_arr[:, 6:12])) + print(f" New CFD force_norm_fact: {new_fn:.6f} (legacy: {norm['force_norm_fact']:.6f})") + + # Bias FIFO + sim.restore() + ema_bias = EMA(weight=0.1) + ema_bias.reset(np.zeros(3, dtype=np.float32)) + for _ in range(FIFO_LEN): + s = ema_bias(bias_omega) + sim.set_body(fid, omega=s[0]); sim.set_body(tid, omega=s[1]); sim.set_body(bid, omega=s[2]) + sim.run(SI, zero_obs=True) + sim.snapshot() # Save DDF AFTER bias (matching legacy env) + + # DRL inference + sim.restore() + ema = EMA(weight=0.1) + ema.reset(bias_omega.copy()) + + model = ModelInventory().load(model_name, device="cpu") + print(f" Model loaded on CPU") + + obs_init = read_obs_legacy(sim, sensor_ids, [fid, tid, bid], cc) + obs_norm = normalize_obs(obs_init, norm) + + sig_s = np.zeros((MAX_STEPS, 6), dtype=np.float32) + sig_f = np.zeros((MAX_STEPS, 6), dtype=np.float32) + sig_a = np.zeros((MAX_STEPS, 3), dtype=np.float32) + + for step in range(MAX_STEPS): + action, _ = model.predict(obs_norm, deterministic=True) + action = np.asarray(action, dtype=np.float32).flatten() + target_omega = action_to_omega(action, scale=4.0, bias=(0.0, -4.0, 4.0)) + smoothed = ema(target_omega) + + sim.set_body(fid, omega=smoothed[0]); sim.set_body(tid, omega=smoothed[1]); sim.set_body(bid, omega=smoothed[2]) + sim.run(SI, zero_obs=True) + + obs = read_obs_legacy(sim, sensor_ids, [fid, tid, bid], cc) + sig_s[step] = obs[0:6] + sig_f[step] = obs[6:12] + sig_a[step] = action + + forces_n = obs[6:12] / norm["force_norm_fact"] + sens_n = (obs[0:6] - norm["sens_deviation"]) / norm["sens_norm_fact"] + obs_norm = np.clip(np.hstack([forces_n, sens_n]), -1, 1).astype(np.float32) + + save_vorticity(sim, os.path.join(out_dir, f"vorticity.png"), + cylinders=[((30.0*L0, CENTER_Y), RADIUS), + ((31.3*L0, CENTER_Y+15.0), RADIUS), + ((31.3*L0, CENTER_Y-15.0), RADIUS)]) + np.savez_compressed(os.path.join(out_dir, "signals.npz"), + sensors=sig_s, forces=sig_f, actions=sig_a) + sim.close() + + # Compare with reference + ref = np.load(os.path.join(ref_dir, "controlled.npz")) + print(f"\n Actions vs ref:") + for i, name in enumerate(["aF","aB","aT"]): + c = np.corrcoef(ref["actions"][:MAX_STEPS,i], sig_a[:,i])[0,1] + print(f" {name}: ref_mean={ref['actions'][:MAX_STEPS,i].mean():+.4f} " + f"our_mean={sig_a[:,i].mean():+.4f} corr={c:+.4f}") + + # DTW similarity: vortex uses step-based rolling, no lag + def dtw_sim(t, s): + n = len(t) + D = np.full((n+1, n+1), np.inf); + D[0,0] = 0 + for i in range(1, n+1): + for j in range(1, n+1): + D[i,j] = abs(t[i-1]-s[j-1]) + min(D[i-1,j], D[i,j-1], D[i-1,j-1]) + return 1 - D[n,n] / n + + sim_val = 0.0 + for i in range(6): + t_rolled = np.roll(target_states[-CONV_LEN:, i], -MAX_STEPS) + sim_val += dtw_sim(t_rolled, sig_s[-CONV_LEN:, i]) / 6.0 + print(f" DTW similarity (our target vs our sensors): {sim_val:.4f}") + + +# ========================================================================= +# Main +# ========================================================================= +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--device", type=int, default=0) + parser.add_argument("--case", type=str, required=True, + choices=["illusion_075L", "illusion_1L", "illusion_15L", + "vortex_lamb", "vortex_taylor"]) + args = parser.parse_args() + + _SRC = os.path.join(os.path.dirname(__file__), "..", "..", "..", "src") + + if "illusion" in args.case: + scenes = { + "illusion_075L": ("d1a3o14_250525_imit_075L_2U_400S", 400, 0.75 * L0, + os.path.join(_SRC, "SR_analysis", "data", "illusion", "illusion_0.75L")), + "illusion_1L": ("d1a3o14_250525_imit_1L_2U_600S", 600, 1.0 * L0, + os.path.join(_SRC, "SR_analysis", "data", "illusion", "illusion_1L")), + "illusion_15L": ("d1a3o14_250525_imit_15L_2U", 800, 1.5 * L0, + os.path.join(_SRC, "SR_analysis", "data", "illusion", "illusion_1.5L")), + } + model_name, si, diam, ref_dir = scenes[args.case] + out_dir = os.path.join(OUT_BASE, args.case) + run_illusion(args.device, args.case, model_name, si, diam, ref_dir, out_dir) + + elif "vortex" in args.case: + vtype = "lamb" if "lamb" in args.case else "taylor" + scenes = { + "vortex_lamb": ("vortex_lamb", 0.5 * U0, + os.path.join(_SRC, "SR_analysis", "data", "vortex", "vortex_lamb")), + "vortex_taylor": ("vortex_taylor", 0.03 * U0, + os.path.join(_SRC, "SR_analysis", "data", "vortex", "vortex_taylor")), + } + model_name, strength, ref_dir = scenes[args.case] + out_dir = os.path.join(OUT_BASE, args.case) + run_vortex(args.device, vtype, model_name, strength, ref_dir, out_dir) + + print("\nDone.") diff --git a/src/drl_pinball/train/TRAIN_KNOWLEDGE.md b/src/drl_pinball/train/TRAIN_KNOWLEDGE.md new file mode 100644 index 0000000..3a7a4f0 --- /dev/null +++ b/src/drl_pinball/train/TRAIN_KNOWLEDGE.md @@ -0,0 +1,420 @@ +# Karman Cloak Training — Knowledge Document + +> **For new developers taking over**: This document contains everything you need to know +> to run, debug, and improve the DRL training pipeline for Karman Cloak on the new +> CelerisLab solver. Read this before touching any code. + +--- + +## 1. What This Codebase Does + +Trains a PPO agent to control a fluidic pinball (3 rotating cylinders) to achieve +hydrodynamic cloaking — making the downstream flow match the "undisturbed" flow +(as if the pinball weren't there). The upstream disturbance cylinder generates a +Kármán vortex street; the pinball must cancel it. + +- **CFD**: CelerisLab LBM solver, D2Q9 MRT, 2000×600 grid, uniform inlet, free-slip walls +- **DRL**: PPO with Sin activation, 64×64 MLP, SB3 + VecNormalize +- **Two training modes**: Bias (with [0,-4,4] action offset) and NoBias (scale=12, no offset) + +--- + +## 2. File Structure (Clean) + +``` +train/ +├── __init__.py +├── env_karman_2000x600.py # Gym environment (final V4) +├── train_karman_2000x600.py # PPO training script (final V4) +├── symmetry_wrapper.py # G-mirror symmetry augmentation (per-rollout) +├── phase0_baseline_measure.py # Stage0/Bias baseline measurement tool +├── analyze_final.py # Training curve plotting & degradation analysis +├── TRAIN_KNOWLEDGE.md # This file +├── target.npy # Pre-recorded target signal (reusable) +├── nohup_bias.log # nohup stdout for current Bias run +├── nohup_nobias.log # nohup stdout for current NoBias run +└── output/ + ├── bias_seed42_s2048_e10_v4/ # Current V4 Bias training (running) + ├── nobias_seed42_s2048_e10_v4/ # Current V4 NoBias training (running) + ├── stage_baseline_2000x600/ # Phase 0 baseline data (reference) + └── degradation_diag/ # Analysis plots from debugging +``` + +--- + +## 3. How to Start Training + +### Prerequisites +- conda env `pycuda_3_10` with PyCUDA, PyTorch, SB3, tensorboard +- CelerisLab installed at `/home/frank14f/CelerisLab` +- `target.npy` in the train directory (pre-recorded, reusable) + +### Starting a training run (CRITICAL: start sequentially, not simultaneously!) + +Two trainings on two GPUs MUST be started one after another with a ~7 min gap, +because they share the same CelerisLab kernel cache. Simultaneous startup causes +kernel compilation race conditions → corrupted kernel → reward=0.000 forever. + +```bash +# 1. Clean stale cache +rm -f /home/frank14f/CelerisLab/src/CelerisLab/lbm/kernels/config/config_objects.h +rm -f /home/frank14f/CelerisLab/src/CelerisLab/lbm/kernels/kernel.ptx + +# 2. Start Bias on GPU0 (with nohup so it survives terminal close) +cd /home/frank14f/DynamisLab/src/drl_pinball/train +nohup conda run --no-capture-output -n pycuda_3_10 python -u train_karman_2000x600.py \ + --device-id 0 --seed 42 --total-episodes 500 --target target.npy \ + > nohup_bias.log 2>&1 & + +# 3. WAIT ~7 minutes for Bias Ep1 to appear (check train.log) +# Confirm reward is non-zero before proceeding! + +# 4. Start NoBias on GPU1 +nohup conda run --no-capture-output -n pycuda_3_10 python -u train_karman_2000x600.py \ + --device-id 1 --seed 42 --total-episodes 500 --target target.npy --no-bias \ + > nohup_nobias.log 2>&1 & +``` + +### Monitoring +```bash +# Check progress +tail -5 output/bias_seed42_s2048_e10_v4/train.log +tail -5 output/nobias_seed42_s2048_e10_v4/train.log + +# TensorBoard +tensorboard --logdir output/bias_seed42_s2048_e10_v4/tb --port 6006 + +# Plot training curves +conda run -n pycuda_3_10 python -u analyze_final.py +``` + +### Stopping +```bash +ps aux | grep train_karman | grep -v grep | awk '{print $2}' | xargs kill +``` + +--- + +## 4. Reward Design (V3 — Current) + +### Formula + +```python +FORCE_SCALE = 0.0025 # fixed physical constant (combined max|force| from Phase 0) + +# Gaussian reward (no zero-crossing spikes that exp(-|x|) causes) +r_cd_raw = exp(-cd_norm² * 50) # cd_norm = (Σfx)/3 / FORCE_SCALE +r_cl_raw = exp(-cl_norm² * 100) # cl_norm = (Σfy)/3 / FORCE_SCALE + +# EMA smoothing for cd/cl only (r_sim is already smooth from DTW) +r_cd = EMA(r_cd_raw, weight=0.2) +r_cl = EMA(r_cl_raw, weight=0.2) + +# Normalized DTW similarity (raw, no EMA) +# norm_scale = mean of target's 3 uy-channel stds (fixed once target is recorded) +r_sim = piecewise_map(sim, breakpoints, values) + +# Floor penalty: prevents DRL from sacrificing one component +if r_cd < 0.10: penalty += 0.05 * (0.10 - r_cd) / 0.10 +if r_cl < 0.10: penalty += 0.05 * (0.10 - r_cl) / 0.10 +if r_sim < 0.10: penalty += 0.05 * (0.10 - r_sim) / 0.10 + +reward = max(0, 0.30*r_cd + 0.30*r_cl + 0.40*r_sim - penalty) +``` + +### Why Gaussian not exp(-|x|) + +`exp(-|x|)` has maximum gradient at x=0. When cd/cl oscillates around zero, +the reward swings wildly (spikes at every zero-crossing). `exp(-x²)` has zero +gradient at x=0, giving smooth reward near the optimum. + +### Why Normalized DTW + +Raw DTW `1 - cost/n` has very narrow dynamic range (0.70-0.97) because cost +depends on absolute signal amplitude. Normalizing by target's uy-channel std +(≈0.20) extends the range to 0.0-0.9, giving DRL a meaningful gradient. + +The norm_scale is computed once from the target signal and is fixed for all +training and inference. It's universal — same formula works for cloak and +illusion (each scenario has its own target → its own norm_scale). + +### Why r_sim has no EMA + +DTW is already a smooth signal (30-step windowed comparison). Adding EMA +over-smoothed it and introduced artificial delay. The 40-step physical delay +(convection 25 steps + DTW window 15 steps) is handled by GAE with gamma=0.995. + +### Three-stage targets (from Phase 0 measurement) + +| Stage | r_cd | r_cl | r_sim | total | Target | +|-------|------|------|-------|-------|--------| +| Stage0 (no rotation) | 0.03 | 0.22 | 0.17 | 0.14 | ~0.2 | +| Stage1 (bias action) | 0.72 | 0.54 | 0.49 | 0.58 | ~0.5 | +| Optimal (trained) | ~0.9 | ~0.9 | ~0.9 | ~0.9 | ~0.9 | + +--- + +## 5. PPO Configuration (V4 — Current) + +```python +PPO( + "MlpPolicy", + policy_kwargs={"activation_fn": Sin, "net_arch": [64, 64]}, + env=vec_env, + device=torch.device("cuda:X"), + n_steps=2048, # MUST be 2048 (legacy default). 512 causes noisy curves. + batch_size=64, # SB3 default + n_epochs=10, # SB3 default. 3 was too few → slow learning. + learning_rate=3e-4, # SB3 default + gamma=0.995, # Higher than SB3 default (0.99) for r_sim delay propagation + # ent_coef=0.0 # SB3 default (not set). n_steps=2048 provides enough diversity. + # target_kl=None # SB3 default (not set). Well-estimated gradients don't need KL stop. + verbose=0, +) +``` + +### Key lesson: Don't deviate from SB3 defaults without strong reason + +V1-V3 used n_steps=512, n_epochs=3, ent_coef=0.01, target_kl=0.03. This caused: +- Noisy training curves (high gradient variance from small n_steps) +- Slow learning (3 epochs too few) +- Degradation after peak (policy oscillation from noisy gradients) + +V4 matches SB3 defaults (n_steps=2048, n_epochs=10, no ent_coef, no target_kl) +and produces smooth, steadily rising curves — matching the legacy training quality. + +### Evaluation: stochastic (NOT deterministic) + +```python +action, _ = model.predict(eval_obs) # NO deterministic=True +``` + +Legacy training used stochastic eval. Deterministic eval gives sharp, noisy +rewards. Stochastic eval averages over the policy distribution → smoother curves. +For paper presentation, stochastic is more honest (shows expected performance). + +### Symmetry: per-rollout, not per-step + +The G-mirror wrapper decides ONCE per rollout (every 2048 steps) whether to +mirror. If mirrored, ALL steps in that rollout use G-transform consistently. +Per-step random mirroring adds noise; per-rollout is clean. + +During evaluation, symmetry is disabled (prob=0.0) for clean policy assessment. + +--- + +## 6. Obs Normalization + +### Two-layer approach + +1. **Env physical norm** (fixed, reproducible): + - forces / FORCE_SCALE (0.0025) + - sensors / SENS_SCALE (0.8, legacy-equiv scale) + - No clipping (VecNormalize handles that) + +2. **SB3 VecNormalize** (running mean/std): + - norm_obs=True, norm_reward=False + - clip_obs=10.0 (generous, physical norm already keeps obs ~[-1,1]) + - Saved to `vec_normalize.pkl` for inference + +### Why not just VecNormalize + +Physical norm ensures the obs is in a reasonable range even before VecNormalize +has collected enough statistics. This prevents extreme values in the first few +hundred steps. The physical norm constants (FORCE_SCALE, SENS_SCALE) are fixed +and do not change between training and inference. + +### Why not just physical norm (like legacy) + +VecNormalize adapts to the actual obs distribution, giving each dimension +equal footing. Without it, sensor ux (range 0.6-0.9) dominates sensor uy +(range 0.2) after physical norm. VecNormalize corrects this. + +--- + +## 7. Action Configuration + +### Bias mode (default) +```python +ACTION_SCALE = 8.0 +ACTION_BIAS = [0, -4, 4] # front, top(+y), bot(-y) +# omega = -(action * 8 + [0,-4,4]) * U0 / RADIUS +# Physical range: front [-8,8], top [-4,12], bot [-12,4] × U0 +``` + +### NoBias mode (--no-bias flag) +```python +ACTION_SCALE = 12.0 +ACTION_BIAS = [0, 0, 0] +# omega = -(action * 12 + [0,0,0]) * U0 / RADIUS +# Physical range: all cylinders [-12,12] × U0 +``` + +NoBias uses scale=12 (not 8) to cover the same omega range as Bias. +With scale=8 and no bias, range is only [-8,8] — missing the ±12×U0 extremes +that Bias reaches. This caused NoBias to fail at learning lift control (r_cl +collapsed to 0.05) because it couldn't reach the necessary rotation speeds. + +### Sign convention (CelerisLab new kernel) + +The new CelerisLab kernel uses `Uw = -omega * ry` (negative sign). +Legacy used `Uw = action * (y_c - y) / radius` (no negative). +Conversion: `omega = -surface_vel / radius` + +--- + +## 8. Environment Design + +### Two-phase initialization (avoids runtime sync_bodies) + +1. `record_target()`: temporary Simulation (dist_cyl + sensors only) → + record 150 steps of target signal → close +2. `KarmanCloakEnv.__init__()`: training Simulation with ALL 7 objects + upfront → warmup → zero-action FIFO → bias FIFO → snapshot + +All objects (1 dist_cyl + 3 sensors + 3 pinball) are added before `initialize()`. +No runtime `sync_bodies()` — it causes recompilation and cache conflicts. + +### Body IDs (add order) +``` +0: dist_cyl (force only, skipped in obs) +1: sensor 0 (top, +y) +2: sensor 1 (center) +3: sensor 2 (bottom, -y) +4: pinball_front +5: pinball_top (rear, +y) +6: pinball_bottom (rear, -y) +``` + +### Obs layout (12-dim, env output before VecNormalize) +``` +[0:6] = forces / FORCE_SCALE: [front_fx, front_fy, top_fx, top_fy, bot_fx, bot_fy] +[6:12] = sensors / SENS_SCALE: [s0_ux, s0_uy, s1_ux, s1_uy, s2_ux, s2_uy] +``` + +### No-reset training + +`step()` always returns `terminated=False`. The flow field is never reset +during training (matches legacy behavior). Episode length is controlled by +the external training loop (`model.learn(2048)` per call). + +Evaluation does `eval_env.reset()` (restores snapshot) for reproducible +assessment. After eval, training continues from the post-eval state. + +--- + +## 9. Bugs Found & Fixed (Full History) + +### Critical bugs + +| # | Bug | Symptom | Fix | +|---|-----|---------|-----| +| 1 | **Simultaneous startup** | Two trainings on 2 GPUs → kernel compilation race → reward=0.000 forever | Start sequentially: Bias first, wait 7 min, then NoBias | +| 2 | **Bias FIFO not applying omega** | `_init_cfd` set bias_omega but never called `set_omega()` during bias FIFO | Added `self._set_omega(bias_omega)` in bias FIFO loop | +| 3 | **CPU training** | `device="cpu"` was used to avoid PyCUDA/PyTorch conflict | Use GPU with `context.push()/pop()` around CFD calls | +| 4 | **Symmetry during eval** | G-mirror prob=0.5 during evaluation → noisy, inaccurate reward | Set `inner.prob = 0.0` before eval, restore after | +| 5 | **DTW not normalized** | Raw DTW `1-cost/n` → sim always 0.90+ → no gradient | Normalize by target uy-channel avg std | +| 6 | **r_sim EMA + delay** | Added unnecessary EMA smoothing and 20-step delay buffer | Removed both — DTW is already smooth, use raw value | +| 7 | **exp(-\|x\|) reward** | Zero-crossing spikes in r_cd/r_cl → noisy training | Changed to Gaussian `exp(-x²*K)` | +| 8 | **NoBias scale=8** | Can't reach ±12×U0 → r_cl collapses | Use scale=12 for NoBias | +| 9 | **n_steps=512** | High gradient variance → noisy curves, degradation | Use n_steps=2048 (SB3 default) | +| 10 | **n_epochs=3** | Too few → slow learning | Use n_epochs=10 (SB3 default) | +| 11 | **deterministic eval** | Sharp, noisy reward measurements | Use stochastic eval (no deterministic=True) | +| 12 | **Stale config_objects.h** | N_OBJS mismatch after previous run | `_clean_cache()` before each Simulation creation | + +### Sensor scaling (from reproduce phase) + +New CelerisLab `read_sensor(normalize=True)` returns area-averaged velocity +(÷cell_count). Legacy returned raw sum (~78x larger). For DTW comparison with +legacy-recorded targets, multiply by `SENSOR_CC=78`. + +### Omega sign inversion (from reproduce phase) + +New kernel: `Uw = -omega * ry` (omega>0 = clockwise). +Legacy: `Uw = action * (y_c - y) / radius` (no sign inversion). +Conversion: `omega = -surface_vel / radius` + +--- + +## 10. Training Results Summary + +### V4 (current, 30 ep before OOM stop — full 500ep running) + +| Training | Ep1 | Ep10 | Ep20 | Ep30 | Smooth? | +|----------|-----|------|------|------|---------| +| Bias | 0.32 | 0.37 | 0.40 | 0.54 | Yes, steady rise | +| NoBias | 0.11 | 0.23 | 0.43 | 0.41 | Yes, near-monotonic | + +### Previous versions (for reference) + +| Version | n_steps | Key change | Result | +|---------|---------|------------|--------| +| V1 | 512 | Config8 reward, exp(-\|x\|) | Peak 0.79 but noisy, possible degradation | +| V2 | 512 | Gaussian + EMA r_sim | Stable but slow (0.62 peak) | +| V3 | 512 | Normalized DTW, no r_sim EMA | Stable, NoBias reached 0.73 | +| **V4** | **2048** | **SB3 defaults, stochastic eval** | **Smoothest curves, running** | + +### NoBias vs Bias + +NoBias requires more episodes to peak (~265 vs ~89 in V3) but can reach +similar or higher peak reward. NoBias r_cl is the main challenge — it needs +the scale=12 action range and floor penalty to prevent collapse. + +--- + +## 11. Key Parameters Reference + +| Parameter | Value | Where | Notes | +|-----------|-------|-------|-------| +| Grid | 2000×600 | config_lbm_karman_2000x600.json | uniform inlet, free_slip | +| U0 | 0.01 | config | lattice inlet velocity | +| ν | 0.004 | config | Re_D=50 (code Re=100) | +| SI | 800 | env | LBM steps per action | +| FIFO_LEN | 150 | env | history buffer | +| CONV_LEN | 30 | env | DTW comparison window | +| FORCE_SCALE | 0.0025 | env | reward normalization | +| SENS_SCALE | 0.8 | env | obs normalization (legacy-equiv) | +| SENSOR_CC | 78 | env | sensor area→legacy conversion | +| K_CD | 50 | env | Gaussian reward coefficient | +| K_CL | 100 | env | Gaussian reward coefficient | +| W_CD/W_CL/W_SIM | 0.30/0.30/0.40 | env | reward weights | +| FLOOR_CD/CL/SIM | 0.10 | env | floor penalty threshold | +| n_steps | 2048 | train | PPO rollout size | +| n_epochs | 10 | train | PPO epochs per update | +| gamma | 0.995 | train | discount factor | +| ACTION_SCALE | 8 (bias) / 12 (nobias) | env | action scaling | +| ACTION_BIAS | [0,-4,4] / [0,0,0] | env | action offset | + +--- + +## 12. CFD Config + +File: `configs/config_lbm_karman_2000x600.json` + +``` +Grid: 2000×600, D2Q9, MRT, double_buffer, FP32 +Inlet: uniform, regularized scheme +Walls: free_slip (matches experimental water tunnel) +Outlet: neq_extrap with backflow_clamp +``` + +### EsoPull note + +EsoPull streaming was tested — it computes correctly but gives ~5% slowdown +(not the expected 50% speedup) on this grid size. Use double_buffer for speed. +EsoPull + zou_he_local inlet gives very different physics (different force +balances) — only use if you re-record the target with the same inlet scheme. + +--- + +## 13. Future Work + +1. **Multi-seed training**: Run 3-5 seeds to show variance bands in paper +2. **Illusion adaptation**: Same pipeline, different target (target cylinder wake). + Need to re-record target, recompute DTW norm_scale, adjust sim breakpoints. +3. **Steady cloak**: Simpler case (no upstream disturbance cylinder). Target is + uniform flow. DTW sim may need different handling (target std≈0). +4. **Symmetry ablation study**: Compare with/without G-mirror to quantify benefit +5. **Longer training**: 500 episodes may not be enough for NoBias to fully learn + r_cl. Try 1000 episodes. +6. **Learning rate schedule**: Try lr decay after peak to prevent degradation diff --git a/src/drl_pinball/train/__init__.py b/src/drl_pinball/train/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/drl_pinball/train/__init__.py @@ -0,0 +1 @@ + diff --git a/src/drl_pinball/train/analyze_final.py b/src/drl_pinball/train/analyze_final.py new file mode 100644 index 0000000..eaca794 --- /dev/null +++ b/src/drl_pinball/train/analyze_final.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +"""Full training analysis: plot curves, verify reward accuracy, analyze degradation. + +Loads TensorBoard data from both Bias and NoBias 500ep trainings, +plots training curves with rolling averages, and analyzes the degradation pattern. + +Usage: + conda run -n pycuda_3_10 python -u analyze_final.py +""" +from __future__ import annotations + +import sys +from pathlib import Path + +import numpy as np +import matplotlib +matplotlib.use('Agg') +import matplotlib.pyplot as plt + +_REPO = Path(__file__).resolve().parents[3] +if str(_REPO) not in sys.path: + sys.path.insert(0, str(_REPO)) + +from tensorboard.backend.event_processing.event_accumulator import EventAccumulator + +OUT_DIR = Path(__file__).resolve().parent / "output" / "degradation_diag" +BIAS_DIR = Path(__file__).resolve().parent / "output" / "bias_seed42_s512_lr0.0003_e3_v3long" +NOBIAS_DIR = Path(__file__).resolve().parent / "output" / "nobias_seed42_s512_lr0.0003_e3_v3long" + + +def load_tb(tb_dir, tag): + ea = EventAccumulator(str(tb_dir)) + ea.Reload() + tags = ea.Tags()['scalars'] + if tag not in tags: + return None + events = ea.Scalars(tag) + by_step = {} + for e in events: + by_step[e.step] = e.value + steps = sorted(by_step.keys()) + return np.array(steps), np.array([by_step[s] for s in steps]) + + +def rolling_avg(vals, window=20): + """Rolling average with edge handling.""" + if len(vals) < window: + return vals + result = np.convolve(vals, np.ones(window)/window, mode='valid') + # Pad edges + pad = (len(vals) - len(result)) // 2 + result = np.pad(result, (pad, len(vals) - len(result) - pad), mode='edge') + return result + + +def main(): + print("=== Final Training Analysis ===") + + all_data = {} + for name, run_dir in [("Bias", BIAS_DIR), ("NoBias", NOBIAS_DIR)]: + data = {} + for tag in ['eval/avg_reward', 'eval/r_cd', 'eval/r_cl', 'eval/r_sim']: + short = tag.split('/')[-1] + result = load_tb(run_dir / "tb", tag) + if result is not None: + data[short] = result + all_data[name] = data + if 'avg_reward' in data: + steps, rewards = data['avg_reward'] + print(f" {name}: {len(steps)} episodes, best={np.max(rewards):.4f} at Ep{steps[np.argmax(rewards)]}") + + # --- Plot 1: Training curves with rolling average --- + fig, axes = plt.subplots(2, 2, figsize=(16, 12)) + fig.suptitle('Training Curves: Bias vs NoBias (V3, 500ep, double_buffer)', fontsize=14) + + titles = { + 'avg_reward': 'Total Reward', + 'r_cd': 'r_cd (drag, Gaussian+EMA)', + 'r_cl': 'r_cl (lift, Gaussian+EMA)', + 'r_sim': 'r_sim (normalized DTW)', + } + colors = {"Bias": "blue", "NoBias": "red"} + + for idx, (key, title) in enumerate(titles.items()): + ax = axes[idx // 2, idx % 2] + for name, data in all_data.items(): + if key not in data: + continue + steps, vals = data[key] + # Raw + ax.plot(steps, vals, alpha=0.2, color=colors[name], linewidth=0.5) + # Rolling average + roll = rolling_avg(vals, window=20) + ax.plot(steps, roll, color=colors[name], linewidth=2, label=f"{name} (20-ep avg)") + # Best line + best_idx = np.argmax(vals) + ax.axhline(vals[best_idx], color=colors[name], linestyle=':', alpha=0.3) + + ax.set_title(title) + ax.set_xlabel("Episode") + ax.legend() + ax.grid(True, alpha=0.3) + + plt.tight_layout() + out1 = OUT_DIR / "final_training_curves.png" + plt.savefig(str(out1), dpi=120) + plt.close() + print(f" Saved: {out1}") + + # --- Plot 2: Degradation analysis --- + fig, axes = plt.subplots(2, 1, figsize=(14, 10)) + fig.suptitle('Degradation Analysis: Pre-peak vs Post-peak', fontsize=14) + + for idx, (name, data) in enumerate(all_data.items()): + if 'avg_reward' not in data: + continue + steps, rewards = data['avg_reward'] + best_idx = np.argmax(rewards) + best_ep = steps[best_idx] + + ax = axes[idx] + ax.plot(steps, rewards, alpha=0.3, color='gray', linewidth=0.5) + ax.plot(steps, rolling_avg(rewards, 20), color='blue', linewidth=2, label='20-ep avg') + ax.axvline(best_ep, color='red', linestyle='--', alpha=0.7, label=f'Peak at Ep{best_ep}') + ax.axhline(rewards[best_idx], color='red', linestyle=':', alpha=0.3, label=f'Best={rewards[best_idx]:.3f}') + + # Mark regions + pre_peak = rewards[:best_idx] + post_peak = rewards[best_idx:] + ax.axhspan(np.mean(post_peak) - np.std(post_peak), + np.mean(post_peak) + np.std(post_peak), + alpha=0.1, color='orange', label=f'Post-peak: {np.mean(post_peak):.3f}±{np.std(post_peak):.3f}') + + ax.set_title(f"{name}: Peak={rewards[best_idx]:.3f} (Ep{best_ep}), Post-peak mean={np.mean(post_peak):.3f}") + ax.set_xlabel("Episode") + ax.legend(fontsize=8) + ax.grid(True, alpha=0.3) + + plt.tight_layout() + out2 = OUT_DIR / "degradation_analysis.png" + plt.savefig(str(out2), dpi=120) + plt.close() + print(f" Saved: {out2}") + + # --- Print detailed analysis --- + print("\n" + "=" * 90) + print("DETAILED ANALYSIS") + print("=" * 90) + + for name, data in all_data.items(): + if 'avg_reward' not in data: + continue + steps, rewards = data['avg_reward'] + best_idx = np.argmax(rewards) + best_ep = steps[best_idx] + + print(f"\n--- {name} ---") + print(f" Total episodes: {len(steps)}") + print(f" Best reward: {rewards[best_idx]:.4f} at Ep{best_ep}") + + # Phase analysis + for phase_name, sl in [("Early (1-50)", slice(0, 50)), + ("Mid (50-best)", slice(50, best_idx)), + (f"Peak (Ep{best_ep})", slice(best_idx, best_idx+1)), + ("Post-peak+50", slice(best_idx, best_idx+50)), + ("Post-peak+100", slice(best_idx, best_idx+100)), + ("Last 50", slice(max(0, len(rewards)-50), len(rewards)))]: + seg_start = sl.start if sl.start is not None else 0 + seg_end = sl.stop if sl.stop is not None else len(rewards) + if seg_start >= len(rewards) or seg_end > len(rewards): + continue + segment = rewards[sl] + if len(segment) == 0: + continue + print(f" {phase_name:>20}: mean={np.mean(segment):.4f}, std={np.std(segment):.4f}, " + f"min={np.min(segment):.4f}, max={np.max(segment):.4f}") + + # Component analysis at peak vs post-peak + for comp in ['r_cd', 'r_cl', 'r_sim']: + if comp not in data: + continue + _, vals = data[comp] + peak_val = vals[best_idx] + post50 = vals[best_idx:best_idx+50] + last50 = vals[-50:] + print(f" {comp}: peak={peak_val:.4f}, post50_mean={np.mean(post50):.4f}, " + f"last50_mean={np.mean(last50):.4f}") + + # --- Reward accuracy check --- + print("\n" + "=" * 90) + print("REWARD ACCURACY CHECK") + print("=" * 90) + print(" Symmetry-off during eval: YES (code verified)") + print(" NoBias Ep1 should be ~0.08 (zero action, no control):") + if 'NoBias' in all_data and 'avg_reward' in all_data['NoBias']: + _, r = all_data['NoBias']['avg_reward'] + print(f" NoBias Ep1 = {r[0]:.4f} -> {'CORRECT' if 0.05 < r[0] < 0.15 else 'SUSPICIOUS'}") + print(" Bias Ep1 should be ~0.55 (bias action, partial control):") + if 'Bias' in all_data and 'avg_reward' in all_data['Bias']: + _, r = all_data['Bias']['avg_reward'] + print(f" Bias Ep1 = {r[0]:.4f} -> {'CORRECT' if 0.50 < r[0] < 0.65 else 'SUSPICIOUS'}") + + print("\n=== Done ===") + + +if __name__ == "__main__": + main() diff --git a/src/drl_pinball/train/env_karman_2000x600.py b/src/drl_pinball/train/env_karman_2000x600.py new file mode 100644 index 0000000..24c487e --- /dev/null +++ b/src/drl_pinball/train/env_karman_2000x600.py @@ -0,0 +1,433 @@ +#!/usr/bin/env python3 +"""Karman Cloak environment for 2000x600 config (uniform, free-slip). + +Design: Two-phase initialization to AVOID runtime sync_bodies(). + Phase 1: Temporary Simulation(dist + sensors) -> record target (legacy-equiv) -> close + Phase 2: Training Simulation(all objects upfront) -> warmup -> bias FIFO -> snapshot + +CUDA context: mirrors legacy pattern — push CFD context before GPU ops, pop after. + This allows PPO (PyTorch) and CFD (PyCUDA) to coexist on the same GPU. + +Observation (12-dim, physical norm, NO clip — VecNormalize handles that): + [0:6] = raw_forces / FORCE_SCALE (front_fx,fy, top_fx,fy, bot_fx,fy) + [6:12] = raw_sensors / SENS_SCALE (s0_ux,uy, s1_ux,uy, s2_ux,uy) + +Action (3-dim): + [-1,1] -> omega = -(action*8 + [0,-4,4]) * U0 / R + +Reward (V2: Gaussian + EMA smoothing + delayed r_sim): + r_cd = EMA(exp(-cd_norm^2 * 50), 0.2) # Gaussian, no zero-crossing spikes + r_cl = EMA(exp(-cl_norm^2 * 100), 0.2) # Gaussian, smoothed + r_sim = delayed_EMA(piecewise_map(sim), 0.05, skip=20) # slow, stable + reward = 0.25*r_cd + 0.25*r_cl + 0.50*r_sim # sim-heavy (most stable) +""" +from __future__ import annotations + +import os, sys, time +from collections import deque +from pathlib import Path +from typing import Optional, Tuple + +import numpy as np +import gymnasium as gym +from gymnasium import spaces + +_REPO = Path(__file__).resolve().parents[3] +if str(_REPO) not in sys.path: + sys.path.insert(0, str(_REPO)) + +from CelerisLab import Simulation + +_CELERIS = Path("/home/frank14f/CelerisLab") +_CONFIG_H = _CELERIS / "src/CelerisLab/lbm/kernels/config/config_objects.h" +_PTX = _CELERIS / "src/CelerisLab/lbm/kernels/kernel.ptx" + +def _clean_cache(): + for p in [_CONFIG_H, _PTX]: + if p.exists(): p.unlink() + +# --------------------------------------------------------------------------- +L0 = 20.0; D_CYL = L0; U0 = 0.01; RADIUS = L0 / 2.0 +NX = 2000; NY = 600 +CENTER_Y = float(NY - 1) / 2.0 + +DIST_X = 600.0 +PINBALL_FRONT_X = 1000.0 +PINBALL_REAR_X = 1026.0 +SENSOR_X = 1200.0 + +SI = 800 +FIFO_LEN = 150; CONV_LEN = 30; MAX_STEPS = 500 +ACTION_SCALE = 8.0 +ACTION_BIAS = np.array([0.0, -4.0, 4.0], dtype=np.float32) + +FORCE_SCALE = np.float32(0.0025) # combined max|force| across Stage0+Bias (Phase 0) +SENS_SCALE = np.float32(0.8) # combined max|sensor| legacy-equiv across stages + +# Reward parameters V3 (Gaussian r_cd/r_cl + normalized DTW + raw r_sim) +K_CD = 50.0 # Gaussian: exp(-cd_norm^2 * K) +K_CL = 100.0 # Gaussian: exp(-cl_norm^2 * K) +EMA_FAST = 0.2 # EMA weight for r_cd/r_cl smoothing (r_sim NOT smoothed) +# Normalized DTW sim mapping (based on Phase 0: Stage0~0.36, Stage1~0.82) +SIM_BP = np.array([0.0, 0.36, 0.82, 0.90, 0.95, 1.0], dtype=np.float64) +SIM_VAL = np.array([0.0, 0.2, 0.5, 0.8, 0.9, 0.95], dtype=np.float64) +W_CD = 0.30 +W_CL = 0.30 +W_SIM = 0.40 + +# Floor penalty: when a component drops below its floor, extra penalty is applied +# This prevents DRL from sacrificing one component to optimize others +FLOOR_CD = 0.10 # r_cd below 0.10 gets penalized +FLOOR_CL = 0.10 # r_cl below 0.10 gets penalized +FLOOR_SIM = 0.10 # r_sim below 0.10 gets penalized +FLOOR_PENALTY = 0.05 # penalty per component below floor (subtracted from reward) + +# NoBias action scale (wider range to cover Bias omega range) +ACTION_SCALE_NOBIAS = 12.0 + +WARMUP_STEPS = int(4.0 * NX / U0) +CFG_PATH = str(_REPO / "configs" / "config_lbm_karman_2000x600.json") +SENSOR_CC = 78.0 + +# --------------------------------------------------------------------------- +def calc_lag(target: np.ndarray, state: np.ndarray) -> int: + t_mean = np.mean(target); s_mean = np.mean(state) + corr = np.correlate(target - t_mean, state - s_mean, mode="full") + lags = np.arange(-len(target) + 1, len(target)) + return int(lags[np.argmax(corr)]) + +def calc_dtw_sim(target: np.ndarray, state: np.ndarray, + norm_scale: float = 1.0) -> float: + """DTW similarity with optional amplitude normalization. + + norm_scale: fixed reference amplitude (e.g. target's uy-avg std). + sim = max(0, 1 - total_cost / (n * norm_scale)) + Without normalization (norm_scale=1.0), this is the raw formula. + """ + n, m = len(target), len(state) + dtw = np.full((n + 1, m + 1), np.inf); dtw[0, 0] = 0.0 + for i in range(1, n + 1): + for j in range(1, m + 1): + cost = abs(float(target[i - 1]) - float(state[j - 1])) + last_min = min(dtw[i - 1, j], dtw[i, j - 1], dtw[i - 1, j - 1]) + dtw[i, j] = cost + last_min + raw = 1.0 - dtw[n, m] / (float(n) * norm_scale) + return float(max(0.0, raw)) + +def compute_similarity(target_states: np.ndarray, fifo_states: np.ndarray, + conv_len: int = CONV_LEN, + norm_scale: float = 1.0) -> float: + """Compute DTW similarity across 6 sensor channels with fixed normalization.""" + target = np.asarray(target_states, dtype=np.float64) + state = np.asarray(fifo_states, dtype=np.float64) + id_sens = 3 + target_seq = target[conv_len:2 * conv_len, id_sens] + state_seq = state[-conv_len:, id_sens] + lag = calc_lag(target_seq, state_seq) + sim = 0.0 + for i in range(6): + t_seq = np.roll(target[:, i], -lag)[conv_len:2 * conv_len] + s_seq = state[-conv_len:, i] + sim += calc_dtw_sim(t_seq, s_seq, norm_scale=norm_scale) + return float(sim / 6.0) + +# --------------------------------------------------------------------------- +class ActionSmoother: + def __init__(self, weight: float = 0.1): + self.weight = weight; self._state: Optional[np.ndarray] = None + def __call__(self, target: np.ndarray) -> np.ndarray: + t = np.asarray(target, dtype=np.float32) + if self._state is None: + self._state = t.copy() + else: + self._state = (1.0 - self.weight) * self._state + self.weight * t + return self._state.copy() + def reset(self, value: Optional[np.ndarray] = None) -> None: + self._state = np.asarray(value, dtype=np.float32).copy() if value is not None else None + +# --------------------------------------------------------------------------- +S_DIM = 12; A_DIM = 3 + +def record_target(device_id: int) -> np.ndarray: + _clean_cache() + sim = Simulation(lbm_config_path=CFG_PATH, device_id=device_id) + sim._assert_object_count_contract = lambda *a, **kw: None + sim.add_body("circle", center=(DIST_X, CENTER_Y, 0.0), radius=1.0 * L0) + s0 = sim.add_body("sensor", center=(SENSOR_X, CENTER_Y + 40.0, 0.0), radius=5.0) + s1 = sim.add_body("sensor", center=(SENSOR_X, CENTER_Y, 0.0), radius=5.0) + s2 = sim.add_body("sensor", center=(SENSOR_X, CENTER_Y - 40.0, 0.0), radius=5.0) + sim.initialize() + sim.run(WARMUP_STEPS, zero_obs=True) + target = np.zeros((FIFO_LEN, 6), dtype=np.float32) + for i in range(FIFO_LEN): + sim.run(SI, zero_obs=True) + target[i] = [ + sim.read_sensor(s0, normalize=True)[0] * SENSOR_CC, + sim.read_sensor(s0, normalize=True)[1] * SENSOR_CC, + sim.read_sensor(s1, normalize=True)[0] * SENSOR_CC, + sim.read_sensor(s1, normalize=True)[1] * SENSOR_CC, + sim.read_sensor(s2, normalize=True)[0] * SENSOR_CC, + sim.read_sensor(s2, normalize=True)[1] * SENSOR_CC, + ] + sim.close() + return np.array(target, dtype=np.float32) + +# --------------------------------------------------------------------------- +class KarmanCloakEnv(gym.Env): + metadata = {"render_modes": ["human"]} + + def __init__(self, device_id: int = 0, seed: int = 42, + target_states: Optional[np.ndarray] = None, + action_bias: Optional[np.ndarray] = None, + action_scale: Optional[float] = None): + super().__init__() + self.device_id = device_id + self.seed = seed + np.random.seed(seed) + + # Configurable action bias and scale + self.action_bias = action_bias if action_bias is not None else ACTION_BIAS.copy() + self.action_scale = action_scale if action_scale is not None else ACTION_SCALE + + self.action_space = spaces.Box(-1.0, 1.0, (A_DIM,), dtype=np.float32) + self.observation_space = spaces.Box(-10.0, 10.0, (S_DIM,), dtype=np.float32) + + self.fifo_states = deque(maxlen=FIFO_LEN) + self.save_states: np.ndarray = None + self.target_states: np.ndarray = None + self.current_step = 0 + self.smoother = ActionSmoother(weight=0.1) + # EMA states for r_cd/r_cl smoothing only (r_sim used raw) + self._ema_r_cd = 0.0 + self._ema_r_cl = 0.0 + # DTW normalization scale (computed from target uy-channel std) + self._dtw_norm_scale = 1.0 + self.sim = None + self.dist_id = None + self.sensor_ids = [] + self.pinball_ids = [] + + if target_states is not None: + self.target_states = target_states + self._init_cfd() + + # ---- Context guard (mirrors legacy context.push()/pop() pattern) ---- + def _ctx_guard(func): + """Decorator: push CFD context before GPU ops, pop after.""" + def wrapper(self, *args, **kwargs): + if self.sim is not None: + self.sim.ctx._ctx.push() + try: + return func(self, *args, **kwargs) + finally: + if self.sim is not None: + self.sim.ctx._ctx.pop() + return wrapper + + # ---- Init ---- + def _init_cfd(self): + t0 = time.perf_counter() + + if self.target_states is None: + self.target_states = record_target(self.device_id) + + # Compute DTW normalization scale from target uy-channel std (fixed) + target_std = np.std(self.target_states, axis=0) + uy_std_avg = float(np.mean([target_std[1], target_std[3], target_std[5]])) + self._dtw_norm_scale = max(uy_std_avg, 0.01) # floor to avoid div-by-zero + print(f" [env] DTW norm scale (target uy-avg std): {self._dtw_norm_scale:.4f}") + + _clean_cache() + self.sim = Simulation(lbm_config_path=CFG_PATH, device_id=self.device_id) + self.sim._assert_object_count_contract = lambda *a, **kw: None + + self.dist_id = self.sim.add_body("circle", center=(DIST_X, CENTER_Y, 0.0), radius=1.0 * L0) + self.sensor_ids = [ + self.sim.add_body("sensor", center=(SENSOR_X, CENTER_Y + 40.0, 0.0), radius=5.0), + self.sim.add_body("sensor", center=(SENSOR_X, CENTER_Y, 0.0), radius=5.0), + self.sim.add_body("sensor", center=(SENSOR_X, CENTER_Y - 40.0, 0.0), radius=5.0), + ] + self.sim.add_body("circle", center=(PINBALL_FRONT_X, CENTER_Y, 0.0), radius=RADIUS) + self.sim.add_body("circle", center=(PINBALL_REAR_X, CENTER_Y + 15.0, 0.0), radius=RADIUS) + self.sim.add_body("circle", center=(PINBALL_REAR_X, CENTER_Y - 15.0, 0.0), radius=RADIUS) + self.sim.initialize() + self.pinball_ids = [4, 5, 6] + + print(f" [env] Warmup ({WARMUP_STEPS} steps)...", end=" ", flush=True) + self._gpu_block(lambda: self.sim.run(WARMUP_STEPS, zero_obs=True)) + print(f"done ({time.perf_counter()-t0:.0f}s).") + + print(f" [env] Zero-action FIFO ({FIFO_LEN})...", end=" ", flush=True) + self._gpu_block(lambda: [self.sim.run(SI, zero_obs=True) for _ in range(FIFO_LEN)]) + f_diag = self._read_obs() + print(f"max|force|={np.max(np.abs(f_diag[6:12])):.6f}") + + bias_omega = self._action_to_omega(np.zeros(3, dtype=np.float32)) + self.smoother.reset(bias_omega.copy()) + print(f" [env] Bias FIFO ({FIFO_LEN})...", end=" ", flush=True) + fifo_save = [] + for _ in range(FIFO_LEN): + self._set_omega(bias_omega) + self._gpu_block(lambda: self.sim.run(SI, zero_obs=True)) + obs = self._read_obs() + sl = obs[2:14].copy() + sl[0:6] *= SENSOR_CC + fifo_save.append(sl) + self.save_states = np.array(fifo_save, dtype=np.float32) + print("done.") + + self._gpu_block(lambda: self.sim.snapshot()) + print(f" [env] Init done ({time.perf_counter()-t0:.0f}s)") + + def _gpu_block(self, fn): + """Execute fn with CFD context pushed, then pop.""" + if self.sim is not None: + self.sim.ctx._ctx.push() + try: + fn() + finally: + if self.sim is not None: + self.sim.ctx._ctx.pop() + + # ---- Private ---- + def _read_obs(self) -> np.ndarray: + """14-dim: [dist_fx,fy, 6×raw_sensor, 6×raw_force].""" + obs = list(self.sim.read_force(self.dist_id, normalize=True)) + for sid in self.sensor_ids: + s = self.sim.read_sensor(sid, normalize=True) + obs.extend([float(s[0]), float(s[1])]) + for pid in self.pinball_ids: + obs.extend(self.sim.read_force(pid, normalize=True)) + return np.array(obs, dtype=np.float32) + + def _action_to_omega(self, action_norm: np.ndarray) -> np.ndarray: + sv = (np.asarray(action_norm, dtype=np.float32) * self.action_scale + self.action_bias) * U0 + return -sv / RADIUS + + def _set_omega(self, omega: np.ndarray): + for pid, w in zip(self.pinball_ids, omega): + self.sim.set_body(pid, omega=float(w)) + + def _normalize_obs(self, raw_obs_slice: np.ndarray) -> np.ndarray: + """Physical normalization (no clip — VecNormalize handles that). + + obs_slice = [6 sensor (legacy-equiv after *CC), 6 force (raw)]. + Returns [forces(6)/FORCE_SCALE, sensors(6)/SENS_SCALE] in env order. + """ + forces = raw_obs_slice[6:12] / FORCE_SCALE + sens = raw_obs_slice[0:6] / SENS_SCALE + return np.hstack([forces, sens]).astype(np.float32) + + def _compute_reward(self, obs_slice: np.ndarray) -> Tuple[float, dict]: + """V3 reward: Gaussian r_cd/r_cl (EMA-smoothed) + raw normalized DTW r_sim. + + obs_slice = [6 sensor (legacy-equiv), 6 force (raw)]. + """ + forces_raw = obs_slice[6:12] + cd_raw = (forces_raw[0] + forces_raw[2] + forces_raw[4]) / 3.0 + cl_raw = (forces_raw[1] + forces_raw[3] + forces_raw[5]) / 3.0 + cd_norm = cd_raw / FORCE_SCALE + cl_norm = cl_raw / FORCE_SCALE + + # Normalized DTW similarity (raw, no EMA — DTW is already smooth) + sim_val = 0.0 + if len(self.fifo_states) >= CONV_LEN * 2: + sim_val = compute_similarity(self.target_states, + np.array(list(self.fifo_states)), + conv_len=CONV_LEN, + norm_scale=self._dtw_norm_scale) + + # Gaussian reward for cd/cl (no zero-crossing spikes) + r_cd_raw = float(np.exp(-cd_norm**2 * K_CD)) + r_cl_raw = float(np.exp(-cl_norm**2 * K_CL)) + + # EMA smoothing only for cd/cl (r_sim used raw — DTW already smooth) + self._ema_r_cd = (1 - EMA_FAST) * self._ema_r_cd + EMA_FAST * r_cd_raw + self._ema_r_cl = (1 - EMA_FAST) * self._ema_r_cl + EMA_FAST * r_cl_raw + r_sim = float(np.interp(sim_val, SIM_BP, SIM_VAL)) + + reward = W_CD * self._ema_r_cd + W_CL * self._ema_r_cl + W_SIM * r_sim + + # Floor penalty: discourage sacrificing any component below its floor + floor_pen = 0.0 + if self._ema_r_cd < FLOOR_CD: + floor_pen += FLOOR_PENALTY * (FLOOR_CD - self._ema_r_cd) / FLOOR_CD + if self._ema_r_cl < FLOOR_CL: + floor_pen += FLOOR_PENALTY * (FLOOR_CL - self._ema_r_cl) / FLOOR_CL + if r_sim < FLOOR_SIM: + floor_pen += FLOOR_PENALTY * (FLOOR_SIM - r_sim) / FLOOR_SIM + reward = max(0.0, reward - floor_pen) + + info = {"cd": float(cd_norm), "cl": float(cl_norm), "sim": float(sim_val), + "r_cd": self._ema_r_cd, "r_cl": self._ema_r_cl, "r_sim": r_sim, + "floor_pen": float(floor_pen)} + return float(reward), info + + # ---- Gym interface ---- + def reset(self, seed=None, options=None) -> Tuple[np.ndarray, dict]: + super().reset(seed=seed) + self._gpu_block(lambda: self.sim.restore()) + self.smoother.reset(self._action_to_omega(np.zeros(3, dtype=np.float32))) + self.fifo_states.clear() + for i in range(len(self.save_states)): + self.fifo_states.append(self.save_states[i, 0:6]) + self.current_step = 0 + # Reset EMA states (r_cd/r_cl only) + self._ema_r_cd = 0.0 + self._ema_r_cl = 0.0 + obs_raw = self._read_obs() + obs = self._normalize_obs(obs_raw[2:14]) + return obs, {} + + def step(self, action: np.ndarray) -> Tuple[np.ndarray, float, bool, bool, dict]: + assert self.action_space.contains(action), f"Invalid action: {action}" + target_omega = self._action_to_omega(action) + smoothed = self.smoother(target_omega) + self._set_omega(smoothed) + + self._gpu_block(lambda: self.sim.run(SI, zero_obs=True)) + + obs_raw = self._read_obs() + obs_slice = obs_raw[2:14] + obs = self._normalize_obs(obs_slice) + self.fifo_states.append(obs_slice[0:6] * SENSOR_CC) + reward, info = self._compute_reward(obs_slice) + + self.current_step += 1 + # Never terminate — flow field stays continuous (legacy pattern). + # Training loop controls episode length externally. + terminated = False + return obs, reward, terminated, False, info + + def render(self, mode="human"): + pass + + def close(self): + if self.sim is not None: + self.sim.close() + +# --------------------------------------------------------------------------- +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser() + parser.add_argument("--device-id", type=int, default=0) + args = parser.parse_args() + + print("=== KarmanCloakEnv 2000x600 Quick Test ===") + env = KarmanCloakEnv(device_id=args.device_id) + + obs, _ = env.reset() + print(f" Init obs: min={obs.min():.4f}, max={obs.max():.4f}, mean={obs.mean():.4f}") + + rewards = [] + for step in range(50): + obs, reward, *_ = env.step(np.zeros(3, dtype=np.float32)) + rewards.append(reward) + print(f" Bias reward (last 20): {np.mean(rewards[-20:]):.4f}") + + obs1, _ = env.reset() + obs2, _ = env.reset() + print(f" Reset consistency: {np.max(np.abs(obs1-obs2)):.8f}") + + env.close() + print("=== Done ===") diff --git a/src/drl_pinball/train/phase0_baseline_measure.py b/src/drl_pinball/train/phase0_baseline_measure.py new file mode 100644 index 0000000..4f23077 --- /dev/null +++ b/src/drl_pinball/train/phase0_baseline_measure.py @@ -0,0 +1,517 @@ +#!/usr/bin/env python3 +"""Phase 0: Baseline measurement for 2000x600 Karman Cloak. + +Collects Stage0 (zero rotation) and Stage1 (bias [0,-4,4]*U0) data to inform +reward design. Records per-step Cd/Cl/Sim and obs norm health. + +Outputs to output/stage_baseline_2000x600/: + - stage_data.pkl : raw obs + forces + sim per stage + - norm_health.json : obs distribution stats + - summary.txt : human-readable comparison table + +Usage: + conda run -n pycuda_3_10 python -u phase0_baseline_measure.py --device-id 0 +""" +from __future__ import annotations + +import argparse +import json +import os +import pickle +import sys +import time +from collections import deque +from pathlib import Path + +import numpy as np +import pycuda.driver as cuda + +cuda.init() + +_REPO = Path(__file__).resolve().parents[3] +if str(_REPO) not in sys.path: + sys.path.insert(0, str(_REPO)) + +from CelerisLab import Simulation + +# --------------------------------------------------------------------------- +# Hard-coded CelerisLab paths for cache cleaning +# --------------------------------------------------------------------------- +_CELERIS = Path("/home/frank14f/CelerisLab") +_CONFIG_H = _CELERIS / "src/CelerisLab/lbm/kernels/config/config_objects.h" +_PTX = _CELERIS / "src/CelerisLab/lbm/kernels/kernel.ptx" + + +def _clean_cache(): + for p in [_CONFIG_H, _PTX]: + if p.exists(): + p.unlink() + + +# --------------------------------------------------------------------------- +# Physics / geometry constants (match env_karman_2000x600.py) +# --------------------------------------------------------------------------- +L0 = 20.0 +D_CYL = L0 +U0 = 0.01 +RADIUS = L0 / 2.0 + +NX = 2000 +NY = 600 +CENTER_Y = float(NY - 1) / 2.0 + +DIST_X = 600.0 +PINBALL_FRONT_X = 1000.0 +PINBALL_REAR_X = 1026.0 +SENSOR_X = 1200.0 + +SI = 800 +FIFO_LEN = 150 +CONV_LEN = 30 +ACTION_SCALE = 8.0 +ACTION_BIAS = np.array([0.0, -4.0, 4.0], dtype=np.float32) + +# Current env norm constants (to assess health) +FORCE_SCALE = np.float32(0.005) +SENS_SCALE = np.float32(U0) + +WARMUP_STEPS = int(4.0 * NX / U0) +CFG_PATH = str(_REPO / "configs" / "config_lbm_karman_2000x600.json") +SENSOR_CC = 78.0 + +N_MEASURE = 100 # steps to measure per stage (after FIFO filled) + +OUT_DIR = Path(__file__).resolve().parent / "output" / "stage_baseline_2000x600" + + +# --------------------------------------------------------------------------- +# DTW utilities (match env_karman_2000x600.py exactly) +# --------------------------------------------------------------------------- +def calc_lag(target: np.ndarray, state: np.ndarray) -> int: + t_mean = np.mean(target) + s_mean = np.mean(state) + corr = np.correlate(target - t_mean, state - s_mean, mode="full") + lags = np.arange(-len(target) + 1, len(target)) + return int(lags[np.argmax(corr)]) + + +def calc_dtw_sim(target: np.ndarray, state: np.ndarray) -> float: + n, m = len(target), len(state) + dtw = np.full((n + 1, m + 1), np.inf) + dtw[0, 0] = 0.0 + for i in range(1, n + 1): + for j in range(1, m + 1): + cost = abs(float(target[i - 1]) - float(state[j - 1])) + last_min = min(dtw[i - 1, j], dtw[i, j - 1], dtw[i - 1, j - 1]) + dtw[i, j] = cost + last_min + return float(1.0 - dtw[n, m] / float(n)) + + +def compute_similarity(target_states: np.ndarray, fifo_states: np.ndarray, + conv_len: int = CONV_LEN) -> float: + target = np.asarray(target_states, dtype=np.float64) + state = np.asarray(fifo_states, dtype=np.float64) + id_sens = 3 # s1_uy (center sensor y-velocity) — matches 2000x600 env + target_seq = target[conv_len:2 * conv_len, id_sens] + state_seq = state[-conv_len:, id_sens] + lag = calc_lag(target_seq, state_seq) + sim = 0.0 + for i in range(6): + t_seq = np.roll(target[:, i], -lag)[conv_len:2 * conv_len] + s_seq = state[-conv_len:, i] + sim += calc_dtw_sim(t_seq, s_seq) + return float(sim / 6.0) + + +# --------------------------------------------------------------------------- +# Context guard +# --------------------------------------------------------------------------- +class CtxGuard: + def __init__(self, sim): + self.sim = sim + + def __enter__(self): + if self.sim is not None: + self.sim.ctx._ctx.push() + + def __exit__(self, *exc): + if self.sim is not None: + self.sim.ctx._ctx.pop() + return False + + +def gpu_block(sim, fn): + with CtxGuard(sim): + fn() + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +def action_to_omega(action_norm: np.ndarray) -> np.ndarray: + """[-1,1] action -> lattice omega (new kernel sign convention).""" + sv = (np.asarray(action_norm, dtype=np.float32) * ACTION_SCALE + ACTION_BIAS) * U0 + return -sv / RADIUS + + +def read_obs(sim, dist_id, sensor_ids, pinball_ids) -> np.ndarray: + """14-dim: [dist_fx,fy, 6x raw_sensor, 6x raw_force].""" + obs = list(sim.read_force(dist_id, normalize=True)) + for sid in sensor_ids: + s = sim.read_sensor(sid, normalize=True) + obs.extend([float(s[0]), float(s[1])]) + for pid in pinball_ids: + obs.extend(sim.read_force(pid, normalize=True)) + return np.array(obs, dtype=np.float32) + + +def set_omega(sim, pinball_ids, omega): + for pid, w in zip(pinball_ids, omega): + sim.set_body(pid, omega=float(w)) + + +def cd_cl_from_obs(obs_slice, force_scale=FORCE_SCALE): + """obs_slice = [6 sensor (legacy-equiv), 6 force (raw)]. + Returns (cd, cl) normalized by force_scale. + """ + forces = obs_slice[6:12] / force_scale + cd = (forces[0] + forces[2] + forces[4]) / 3.0 + cl = (forces[1] + forces[3] + forces[5]) / 3.0 + return float(cd), float(cl) + + +# --------------------------------------------------------------------------- +# Stage runner +# --------------------------------------------------------------------------- +def run_stage(sim, dist_id, sensor_ids, pinball_ids, target_states, + omega_vec, stage_name, n_measure=N_MEASURE): + """Run one stage: fill FIFO with given omega, then measure n_measure steps. + + Returns dict with per-step obs_slice, cd, cl, sim, and FIFO. + """ + print(f" [{stage_name}] Filling FIFO ({FIFO_LEN} x SI)...", end=" ", flush=True) + t0 = time.perf_counter() + fifo = deque(maxlen=FIFO_LEN) + with CtxGuard(sim): + set_omega(sim, pinball_ids, omega_vec) + for _ in range(FIFO_LEN): + sim.run(SI, zero_obs=True) + obs = read_obs(sim, dist_id, sensor_ids, pinball_ids) + sl = obs[2:14].copy() + sl[0:6] *= SENSOR_CC # legacy-equiv for DTW + fifo.append(sl) + print(f"done ({time.perf_counter()-t0:.0f}s).") + + print(f" [{stage_name}] Measuring ({n_measure} steps)...", end=" ", flush=True) + t0 = time.perf_counter() + obs_slices = [] + cds, cls, sims = [], [], [] + with CtxGuard(sim): + set_omega(sim, pinball_ids, omega_vec) + for _ in range(n_measure): + sim.run(SI, zero_obs=True) + obs = read_obs(sim, dist_id, sensor_ids, pinball_ids) + sl = obs[2:14].copy() + sl[0:6] *= SENSOR_CC + fifo.append(sl) + obs_slices.append(sl.copy()) + cd, cl = cd_cl_from_obs(sl) + cds.append(cd) + cls.append(cl) + # Sim requires >= CONV_LEN*2 samples in fifo + sim_val = compute_similarity(target_states, np.array(list(fifo))) + sims.append(sim_val) + print(f"done ({time.perf_counter()-t0:.0f}s).") + + return { + "obs_slices": np.array(obs_slices, dtype=np.float32), # (N, 12) + "cds": np.array(cds, dtype=np.float32), + "cls": np.array(cls, dtype=np.float32), + "sims": np.array(sims, dtype=np.float32), + "fifo": np.array(list(fifo), dtype=np.float32), # (FIFO_LEN, 12) + } + + +# --------------------------------------------------------------------------- +# Norm health +# --------------------------------------------------------------------------- +def norm_health(obs_slices, force_scale=FORCE_SCALE, sens_scale=SENS_SCALE): + """Assess obs normalization health. + + obs_slices: (N, 12) where [0:6]=sensor (legacy-equiv), [6:12]=force (raw). + Returns dict with per-dim stats after normalization. + """ + # Normalize the same way env does (but env uses raw sensor, not legacy-equiv; + # for norm health we check the RAW values that env actually normalizes) + # Note: env _normalize_obs uses obs_slice[0:6] (raw sensor) / SENS_SCALE + # and obs_slice[6:12] (raw force) / FORCE_SCALE. + # But our obs_slices here have sensor already *CC. For norm health we need RAW. + # We stored sensor*CC for DTW; for norm health we divide back. + raw = obs_slices.copy() + raw[:, 0:6] = raw[:, 0:6] / SENSOR_CC # back to raw (area-normalized) sensor + + forces_n = raw[:, 6:12] / force_scale + sens_n = raw[:, 0:6] / sens_scale + normed = np.hstack([forces_n, sens_n]) # (N, 12) [forces(6), sens(6)] — env order + + names = ["f_front_fx", "f_front_fy", "f_top_fx", "f_top_fy", "f_bot_fx", "f_bot_fy", + "s0_ux", "s0_uy", "s1_ux", "s1_uy", "s2_ux", "s2_uy"] + stats = {} + clip_count = 0 + total = normed.size + for i, name in enumerate(names): + col = normed[:, i] + clipped = np.sum(np.abs(col) > 1.0) + clip_count += clipped + stats[name] = { + "mean": float(np.mean(col)), + "std": float(np.std(col)), + "min": float(np.min(col)), + "max": float(np.max(col)), + "clip_ratio": float(clipped / len(col)), + } + stats["_overall"] = { + "clip_ratio_total": float(clip_count / total), + "force_clip": float(np.sum(np.abs(forces_n) > 1.0) / forces_n.size), + "sens_clip": float(np.sum(np.abs(sens_n) > 1.0) / sens_n.size), + } + return stats, normed + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- +def main() -> int: + parser = argparse.ArgumentParser(description="Phase 0 Baseline Measure") + parser.add_argument("--device-id", type=int, default=0) + args = parser.parse_args() + + OUT_DIR.mkdir(parents=True, exist_ok=True) + log_path = OUT_DIR / "phase0.log" + + def log(msg, **kwargs): + line = f"[{time.strftime('%H:%M:%S')}] {msg}" + print(line, **kwargs) + with open(log_path, "a") as f: + f.write(line + "\n") + f.flush() + + log(f"=== Phase 0 Baseline Measure (2000x600, device={args.device_id}) ===") + log(f" Output: {OUT_DIR}") + + # ---- Step 1: Record target (dist_cyl + 3 sensors, no pinball) ---- + log(" Step 1: Recording target signal...") + _clean_cache() + sim = Simulation(lbm_config_path=CFG_PATH, device_id=args.device_id) + sim._assert_object_count_contract = lambda *a, **kw: None + dist_id = sim.add_body("circle", center=(DIST_X, CENTER_Y, 0.0), radius=1.0 * L0) + s0 = sim.add_body("sensor", center=(SENSOR_X, CENTER_Y + 40.0, 0.0), radius=5.0) + s1 = sim.add_body("sensor", center=(SENSOR_X, CENTER_Y, 0.0), radius=5.0) + s2 = sim.add_body("sensor", center=(SENSOR_X, CENTER_Y - 40.0, 0.0), radius=5.0) + sensor_ids = [s0, s1, s2] + sim.initialize() + + log(f" Warmup target sim ({WARMUP_STEPS} steps)...", end=" ", flush=True) + t0 = time.perf_counter() + with CtxGuard(sim): + sim.run(WARMUP_STEPS, zero_obs=True) + log(f"done ({time.perf_counter()-t0:.0f}s).") + + target = np.zeros((FIFO_LEN, 6), dtype=np.float32) + with CtxGuard(sim): + for i in range(FIFO_LEN): + sim.run(SI, zero_obs=True) + target[i] = [ + sim.read_sensor(s0, normalize=True)[0] * SENSOR_CC, + sim.read_sensor(s0, normalize=True)[1] * SENSOR_CC, + sim.read_sensor(s1, normalize=True)[0] * SENSOR_CC, + sim.read_sensor(s1, normalize=True)[1] * SENSOR_CC, + sim.read_sensor(s2, normalize=True)[0] * SENSOR_CC, + sim.read_sensor(s2, normalize=True)[1] * SENSOR_CC, + ] + sim.close() + log(f" Target recorded. s1_uy range: [{target[:,3].min():.4f}, {target[:,3].max():.4f}], std={target[:,3].std():.4f}") + + # ---- Step 2: Create training sim with ALL objects ---- + log(" Step 2: Creating training sim (all 7 objects)...") + _clean_cache() + sim = Simulation(lbm_config_path=CFG_PATH, device_id=args.device_id) + sim._assert_object_count_contract = lambda *a, **kw: None + dist_id = sim.add_body("circle", center=(DIST_X, CENTER_Y, 0.0), radius=1.0 * L0) + sensor_ids = [ + sim.add_body("sensor", center=(SENSOR_X, CENTER_Y + 40.0, 0.0), radius=5.0), + sim.add_body("sensor", center=(SENSOR_X, CENTER_Y, 0.0), radius=5.0), + sim.add_body("sensor", center=(SENSOR_X, CENTER_Y - 40.0, 0.0), radius=5.0), + ] + sim.add_body("circle", center=(PINBALL_FRONT_X, CENTER_Y, 0.0), radius=RADIUS) + sim.add_body("circle", center=(PINBALL_REAR_X, CENTER_Y + 15.0, 0.0), radius=RADIUS) + sim.add_body("circle", center=(PINBALL_REAR_X, CENTER_Y - 15.0, 0.0), radius=RADIUS) + sim.initialize() + pinball_ids = [4, 5, 6] + + log(f" Warmup pinball sim ({WARMUP_STEPS} steps)...", end=" ", flush=True) + t0 = time.perf_counter() + with CtxGuard(sim): + sim.run(WARMUP_STEPS, zero_obs=True) + log(f"done ({time.perf_counter()-t0:.0f}s).") + + # Snapshot at zero-action state (for restoring between stages) + with CtxGuard(sim): + sim.snapshot() + log(" Snapshot saved (zero-action pinball state).") + + # ---- Step 3: Stage 0 — zero rotation ---- + log(" Step 3: Stage 0 (zero rotation)...") + with CtxGuard(sim): + sim.restore() + zero_omega = np.zeros(3, dtype=np.float32) + stage0 = run_stage(sim, dist_id, sensor_ids, pinball_ids, target, + zero_omega, "Stage0") + + # ---- Step 4: Stage 1 — bias [0,-4,4]*U0 ---- + log(" Step 4: Stage 1 (bias [0,-4,4]*U0)...") + with CtxGuard(sim): + sim.restore() + # bias action_norm = [0,0,0] -> omega = -(0*8 + [0,-4,4])*U0 / R + bias_omega = action_to_omega(np.zeros(3, dtype=np.float32)) + log(f" bias_omega = {bias_omega}") + stage1 = run_stage(sim, dist_id, sensor_ids, pinball_ids, target, + bias_omega, "Stage1") + + sim.close() + + # ---- Step 5: Analysis ---- + log(" Step 5: Analysis...") + + # 5a. Cd/Cl/Sim summary per stage + def stage_summary(stage, name): + cd_mean, cd_std = float(np.mean(stage["cds"])), float(np.std(stage["cds"])) + cl_mean, cl_std = float(np.mean(stage["cls"])), float(np.std(stage["cls"])) + sim_mean, sim_std = float(np.mean(stage["sims"])), float(np.std(stage["sims"])) + # Current reward formula + r_cd = float(np.mean(np.exp(-np.abs(stage["cds"] * 20)))) + r_cl = float(np.mean(np.exp(-np.abs(stage["cls"] * 80)))) + r_sim = float(np.mean(np.exp(-10 * np.abs(stage["sims"] - 1)))) + reward = float(np.mean(np.minimum(0.3 * np.exp(-np.abs(stage["cds"] * 20)) + + 0.4 * np.exp(-np.abs(stage["cls"] * 80)) + + 0.3 * np.exp(-10 * np.abs(stage["sims"] - 1)), 1.0))) + return { + "cd_mean": cd_mean, "cd_std": cd_std, + "cl_mean": cl_mean, "cl_std": cl_std, + "sim_mean": sim_mean, "sim_std": sim_std, + "r_cd": r_cd, "r_cl": r_cl, "r_sim": r_sim, "reward": reward, + } + + s0_sum = stage_summary(stage0, "Stage0") + s1_sum = stage_summary(stage1, "Stage1") + + # 5b. Force range (for "combined range" normalization design) + def force_range(stage): + f = stage["obs_slices"][:, 6:12] # raw forces + return { + "max_abs_per_dim": [float(np.max(np.abs(f[:, i]))) for i in range(6)], + "max_abs_combined": float(np.max(np.abs(f))), + "mean_abs_combined": float(np.mean(np.abs(f))), + "std_combined": float(np.std(f)), + } + + def sensor_range(stage): + s = stage["obs_slices"][:, 0:6] # legacy-equiv sensors + return { + "max_abs_per_dim": [float(np.max(np.abs(s[:, i]))) for i in range(6)], + "max_abs_combined": float(np.max(np.abs(s))), + "mean_abs_combined": float(np.mean(np.abs(s))), + "std_combined": float(np.std(s)), + } + + fr0, fr1 = force_range(stage0), force_range(stage1) + sr0, sr1 = sensor_range(stage0), sensor_range(stage1) + + # 5c. Norm health (using current FORCE_SCALE=0.005, SENS_SCALE=U0) + nh0, normed0 = norm_health(stage0["obs_slices"]) + nh1, normed1 = norm_health(stage1["obs_slices"]) + + # ---- Print summary ---- + lines = [] + lines.append("=" * 100) + lines.append("PHASE 0 BASELINE MEASURE SUMMARY (2000x600)") + lines.append("=" * 100) + + lines.append("\n--- Target signal stats ---") + names_s = ["s0_ux", "s0_uy", "s1_ux", "s1_uy", "s2_ux", "s2_uy"] + for i, n in enumerate(names_s): + lines.append(f" {n:>8}: mean={target[:,i].mean():.4f}, std={target[:,i].std():.4f}, " + f"range=[{target[:,i].min():.4f}, {target[:,i].max():.4f}]") + + lines.append("\n--- Stage comparison: Cd / Cl / Sim / Reward ---") + lines.append(f" {'Stage':>8} {'Cd_mean':>10} {'Cd_std':>10} {'Cl_mean':>10} {'Cl_std':>10} " + f"{'Sim_mean':>10} {'Sim_std':>10} {'r_cd':>8} {'r_cl':>8} {'r_sim':>8} {'reward':>8}") + for name, s in [("Stage0", s0_sum), ("Stage1", s1_sum)]: + lines.append(f" {name:>8} {s['cd_mean']:>10.4f} {s['cd_std']:>10.4f} {s['cl_mean']:>10.4f} " + f"{s['cl_std']:>10.4f} {s['sim_mean']:>10.4f} {s['sim_std']:>10.4f} " + f"{s['r_cd']:>8.4f} {s['r_cl']:>8.4f} {s['r_sim']:>8.4f} {s['reward']:>8.4f}") + + lines.append("\n--- Force range (raw, for combined-range norm design) ---") + fnames = ["front_fx", "front_fy", "top_fx", "top_fy", "bot_fx", "bot_fy"] + for name, fr in [("Stage0", fr0), ("Stage1", fr1)]: + lines.append(f" {name}: max_abs_combined={fr['max_abs_combined']:.6f}, " + f"mean_abs={fr['mean_abs_combined']:.6f}, std={fr['std_combined']:.6f}") + for i, fn in enumerate(fnames): + lines.append(f" {fn:>10}: max_abs={fr['max_abs_per_dim'][i]:.6f}") + + lines.append("\n--- Sensor range (legacy-equiv, for combined-range norm design) ---") + for name, sr in [("Stage0", sr0), ("Stage1", sr1)]: + lines.append(f" {name}: max_abs_combined={sr['max_abs_combined']:.4f}, " + f"mean_abs={sr['mean_abs_combined']:.4f}, std={sr['std_combined']:.4f}") + for i, sn in enumerate(names_s): + lines.append(f" {sn:>8}: max_abs={sr['max_abs_per_dim'][i]:.4f}") + + lines.append("\n--- Obs norm health (current: FORCE_SCALE=0.005, SENS_SCALE=U0=0.01) ---") + nh_names = ["f_front_fx", "f_front_fy", "f_top_fx", "f_top_fy", "f_bot_fx", "f_bot_fy", + "s0_ux", "s0_uy", "s1_ux", "s1_uy", "s2_ux", "s2_uy"] + for name, nh in [("Stage0", nh0), ("Stage1", nh1)]: + lines.append(f"\n {name} (env order: forces[6], sensors[6]):") + lines.append(f" {'dim':>14} {'mean':>10} {'std':>10} {'min':>10} {'max':>10} {'clip%':>8}") + for i, n in enumerate(nh_names): + d = nh[n] + lines.append(f" {n:>14} {d['mean']:>10.4f} {d['std']:>10.4f} {d['min']:>10.4f} " + f"{d['max']:>10.4f} {d['clip_ratio']*100:>7.1f}%") + ov = nh["_overall"] + lines.append(f" {'OVERALL':>14} clip_ratio_total={ov['clip_ratio_total']*100:.1f}%, " + f"force_clip={ov['force_clip']*100:.1f}%, sens_clip={ov['sens_clip']*100:.1f}%") + + lines.append("\n--- Gradient assessment ---") + lines.append(f" Cd: Stage0={s0_sum['cd_mean']:.4f} -> Stage1={s1_sum['cd_mean']:.4f} " + f"(Δ={s1_sum['cd_mean']-s0_sum['cd_mean']:+.4f})") + lines.append(f" Cl: Stage0={s0_sum['cl_mean']:.4f} -> Stage1={s1_sum['cl_mean']:.4f} " + f"(Δ={s1_sum['cl_mean']-s0_sum['cl_mean']:+.4f})") + lines.append(f" Sim: Stage0={s0_sum['sim_mean']:.4f} -> Stage1={s1_sum['sim_mean']:.4f} " + f"(Δ={s1_sum['sim_mean']-s0_sum['sim_mean']:+.4f})") + lines.append(f" Reward: Stage0={s0_sum['reward']:.4f} -> Stage1={s1_sum['reward']:.4f} " + f"(Δ={s1_sum['reward']-s0_sum['reward']:+.4f})") + + summary = "\n".join(lines) + print("\n" + summary) + with open(OUT_DIR / "summary.txt", "w") as f: + f.write(summary + "\n") + + # Save raw data + with open(OUT_DIR / "stage_data.pkl", "wb") as f: + pickle.dump({ + "target": target, + "stage0": stage0, "stage1": stage1, + "s0_summary": s0_sum, "s1_summary": s1_sum, + "force_range": {"stage0": fr0, "stage1": fr1}, + "sensor_range": {"stage0": sr0, "stage1": sr1}, + }, f) + + with open(OUT_DIR / "norm_health.json", "w") as f: + json.dump({"stage0": nh0, "stage1": nh1}, f, indent=2) + + log(f"\nPhase 0 complete. Files in {OUT_DIR}:") + log(f" summary.txt, stage_data.pkl, norm_health.json, phase0.log") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/drl_pinball/train/prompt.md b/src/drl_pinball/train/prompt.md new file mode 100644 index 0000000..8e456ab --- /dev/null +++ b/src/drl_pinball/train/prompt.md @@ -0,0 +1,11 @@ +这是我的课题项目库。我的大方向是机器学习和主动流动控制。CelerisLab是我写的LBM CFD库,而DynamisLab是结合机器学习的库。之前我已经做了很多工作,针对pinball(三圆柱)旋转控制,在各种雷诺数下用DRL实现了stealth和illusion,以及erase的尝试。具体而言,我目前关注二维流动,pinball是三个等间距分布的圆柱,我通过测量圆柱水动力和下游流速传感器,设置不同reward,让DRL学习控制策略,实现对下游流场的控制。下游跟上游特征一致即为隐身,下游特征跟设计物体表现一致即为欺骗,抹除上游特征即为erase。我还尝试了DANTE-SINDy结合进行高效策略搜索。总之,我需要你先完整探索我的工作。第一步先阅读@docs/My_Confirmation/Chapters/2.ProblemDescription.tex @docs/My_Confirmation/Chapters/3.Methodology.tex @docs/My_Confirmation/Chapters/4.Result.tex 我的confirmation报告,先有一个大致的了解。然后大概看一下@LegacyCelerisLab/driver.py 这个旧CFD,以及drl_pinball文件夹内的旧脚本和部分重构的新脚本@src/drl_pinball/knowledge.md ,注意所有旧脚本CFD和相关配置的导入都变动了,因为现在在全新的工作库内 @src/understanding_notes.md 。着重理解每个脚本的头注释以及相关代码,每个case差异不大,所以你理解透彻其实工作量不大。然后我补充一些之前提供给agent的信息:要注意erase是没有完全实现的部分,然后入口流虽然说是均匀,但不是uniform,而是parabolic,因为你看legacy config,我当时只能实现no-slip wall,所以入口也都是抛物线。然后网格数别算错了,旧版U单位不是L0,而是cuda config里写的,是为了对齐SM数。SAMPLE_INTERVAL也不只这个范围,具体要根据模型名,其他情况对应脚本内的步数。Re=50注意是不是按照pinball单个圆柱直径算的?其实这个我叫做Re_D,但其他所有写Re的时候是按照两倍直径算的。所以其实默认场景下应该是Re100。然后还有一个关键信息,我所有环境应该都有一个算norm的过程,也就是采集obs的最大波动,这是为了传递给DRL时尽量数值分布均匀。这不是一个好设计但是你要注意我之前都是这么做的,使用模型的时候也需要这个数据。然后pinball的位置不一定(主要是为了在训练时利用好计算域,现在我们主要做推理和分析,其实你可以考虑固定pinball位置),但是pinball到扰流圆柱和sensor之间的距离基本不变,除了illusion情况下,为了贴近目标圆柱的等效距离,与sensor之间的距离有微调。Dummy确实是仅用于匹配的,不过要小心norm不一定是零动作,有可能是带偏置的匀速,每个系列都不一样要注意。自行搜索调研,理解我的工作。然后简单说一下你的想法 + +我想跟你再描述一遍:两个方向cloak和illusion。cloak主要针对两个场景,steady和Karman,也就是pinball上游是均匀还是涡街。两个场景下pinball不控制的话,均匀来流时pinball会产生涡街,涡街来流时pinball下游会比较乱没有明显周期性。cloak控制下,均匀来流时pinball上下两个圆柱定速旋转,下游保持均匀稳定,涡街来流时三个圆柱周期性改变转速,下游维持涡街频率和形状。illusion下,不控制跟刚才描述一致,产生涡街,控制下,三个圆柱周期性变速,产生与不控制不同频率的涡街,对应目标特定直径圆柱产生的涡街。 +首先我需要你熟悉新CFD库@CelerisLab/README.md ,尤其是test里的一些测试,熟悉使用方法。然后看一下reproduce的内容@src/drl_pinball/reproduce/REPRODUCE_KNOWLEDGE.md 。以及上一轮尝试在本地迁移训练,找合适配置的过程: @src/drl_pinball/train/TRAIN_KNOWLEDGE.md 。 +我再跟你讲一下之前legacy训练的经验。 +1. 最重要的是DRL引入Sin激活函数。由于可以预计结果是周期性的,所以在网络中引入能大幅增强。 +2. 第二重要的是训练过程中不reset,只要不崩溃就尽可能不reset环境,让DRL持续尝试控制。 +3. reward的设计和组成,主要有瞬时的合力为零,辅助的相似度最高,然后为了让训练过程中不同阶段敏感性不同,采用了一些缩放。 +4. 然后是obs-act的偏置和缩放,为了让数值尽可能利用好范围。同时act的偏置是经验性的,贡献很大。但我很怀疑这个做法的优势,因为之前采样得到norm导致复现困难,尤其是实验环境中不可能得到相同norm。 +5. 训练超参数好像也有点用,但是我不知道为什么有帮助。 +现在我要你关注新train阶段,主要问题是之前agent工作质量非常差,我认为主要问题在于reward设计,我希望三个组成部分。Cd Cl DTW在三个阶段 不旋转、偏置旋转、最优控制下,都大致能给出0.2、0.5、0.9的表现。上一轮这个目标一直做不好,然后就开始训练并且没什么效果(因为偏置本身就能有不错的效果,DRL只要幅度不大效果也不会差多少)。DTW本身我希望是通用的,也就是cloak和illusion都用同一个DTW算法,这样相似度才有价值。但是你可以缩放、映射成符合我需求的reward表现。还有是你要检查DRL看到的obs是否合适,也就是norm做的怎么样,我还有疑问如果SB3内部能做norm,我env做norm合适吗。以及其他很多细节,甚至我有个想法,既然很明显这个问题是上下对称的,看SR分析时强制用的G变换。有没有可能在DRL也引入这个机制引导让上下控制率一样。比如来回交换上下的obs-act给DRL,让他更容易学到对称的解。最后才是超参数配置,我其实不熟悉机器学习,很需要你的帮助。你现在对我的目的清楚了吗,再跟我讨论一下你的想法吧 \ No newline at end of file diff --git a/src/drl_pinball/train/symmetry_wrapper.py b/src/drl_pinball/train/symmetry_wrapper.py new file mode 100644 index 0000000..a14f9c6 --- /dev/null +++ b/src/drl_pinball/train/symmetry_wrapper.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +"""Symmetry augmentation wrapper for Karman Cloak (2000x600). + +Applies the G-operator (up/down mirror) with 50% probability to encourage +the DRL policy to learn a symmetric control law. + +Obs layout (env output, physical norm): [forces(6), sensors(6)] + forces = [f_front_fx, f_front_fy, f_top_fx, f_top_fy, f_bot_fx, f_bot_fy] + sensors = [s0_ux, s0_uy, s1_ux, s1_uy, s2_ux, s2_uy] + (s0=top/+y, s1=center, s2=bot/-y; top=+y, bot=-y) + +Action layout: [a_front, a_top, a_bot] + +G-operator (y -> -y mirror): + obs forces: front_fx stays, front_fy negated + top <-> bot swap, fy negated + obs sensors: s0(top) <-> s2(bot) swap, uy negated + s1(center) ux stays, uy negated + action: [aF, aT, aB] -> [-aF, -aB, -aT] + +Wrapper placement: between KarmanCloakEnv and DummyVecEnv (inside VecNormalize). +The wrapper intercepts step(): with 50% prob, applies G to obs seen by policy, +then applies G_inv to action before passing to env. Reward and done are +unchanged (G is a symmetry of the physics). + +Note: This wrapper must be applied BEFORE VecNormalize so that VecNormalize +sees the (possibly G-transformed) obs. Since G is an exact symmetry, the +running mean/std statistics remain valid (G just swaps/sign-flips dimensions, +preserving the overall distribution shape). + +Usage in train script: + env = KarmanCloakEnv(...) + env = SymmetryAugmentWrapper(env, prob=0.5) + vec_env = DummyVecEnv([lambda: env]) + vec_env = VecNormalize(vec_env, ...) +""" +from __future__ import annotations + +from typing import Tuple + +import numpy as np +import gymnasium as gym +from gymnasium import spaces + + +def apply_G_obs(obs: np.ndarray) -> np.ndarray: + """Apply G-operator to obs [forces(6), sensors(6)]. + + Returns G(obs) with same shape. G is its own inverse (G^2 = identity). + """ + out = np.empty_like(obs) + # Forces: [f_front_fx, f_front_fy, f_top_fx, f_top_fy, f_bot_fx, f_bot_fy] + out[0] = obs[0] # front_fx stays + out[1] = -obs[1] # front_fy negated + out[2] = obs[4] # top_fx <- bot_fx + out[3] = -obs[5] # top_fy <- -bot_fy + out[4] = obs[2] # bot_fx <- top_fx + out[5] = -obs[3] # bot_fy <- -top_fy + # Sensors: [s0_ux, s0_uy, s1_ux, s1_uy, s2_ux, s2_uy] + out[6] = obs[10] # s0_ux <- s2_ux + out[7] = -obs[11] # s0_uy <- -s2_uy + out[8] = obs[8] # s1_ux stays + out[9] = -obs[9] # s1_uy negated + out[10] = obs[6] # s2_ux <- s0_ux + out[11] = -obs[7] # s2_uy <- -s0_uy + return out + + +def apply_G_action(action: np.ndarray) -> np.ndarray: + """Apply G-operator to action [a_front, a_top, a_bot]. + + [aF, aT, aB] -> [-aF, -aB, -aT] + G is its own inverse (G^2 = identity). + """ + out = np.empty_like(action) + out[0] = -action[0] # front reversed + out[1] = -action[2] # top <- -bot + out[2] = -action[1] # bot <- -top + return out + + +class SymmetryAugmentWrapper(gym.Wrapper): + """Augment training data with G-operator symmetry (per-rollout, 50% prob). + + Instead of randomly mirroring each step, this wrapper decides ONCE per + rollout (every `rollout_len` steps) whether to mirror. If mirrored, ALL + steps in that rollout use G-transform consistently. This avoids the noise + of per-step random switching. + + reset() always returns non-mirrored obs (for clean eval). + """ + + def __init__(self, env: gym.Env, prob: float = 0.5, seed: int = 42, + rollout_len: int = 2048): + super().__init__(env) + self.prob = prob + self._rollout_len = rollout_len + self._rng = np.random.default_rng(seed) + self._mirrored = False + self._step_count = 0 + + def reset(self, *, seed=None, options=None) -> Tuple[np.ndarray, dict]: + obs, info = self.env.reset(seed=seed, options=options) + self._mirrored = False # eval always non-mirrored + self._step_count = 0 + return obs, info + + def step(self, action: np.ndarray) -> Tuple[np.ndarray, float, bool, bool, dict]: + # If currently mirrored, policy's action is in mirrored frame + if self._mirrored: + real_action = apply_G_action(action) + else: + real_action = action + + obs, reward, terminated, truncated, info = self.env.step(real_action) + + # Decide mirror state for next rollout boundary + self._step_count += 1 + if self._step_count % self._rollout_len == 0: + # Start of new rollout — flip coin + self._mirrored = self._rng.random() < self.prob + + # Apply mirror to obs if in mirrored mode + if self._mirrored: + obs = apply_G_obs(obs) + + return obs, reward, terminated, truncated, info + + +# --------------------------------------------------------------------------- +# Verification +# --------------------------------------------------------------------------- +def _verify_g_involution(): + """G(G(x)) should equal x for both obs and action.""" + rng = np.random.default_rng(0) + # Test obs + for _ in range(100): + obs = rng.standard_normal(12) + assert np.allclose(apply_G_obs(apply_G_obs(obs)), obs), "G_obs not involution" + # Test action + for _ in range(100): + act = rng.standard_normal(3) + assert np.allclose(apply_G_action(apply_G_action(act)), act), "G_action not involution" + print(" G is involution (G^2 = identity): PASS") + + +def _verify_symmetry_semantics(): + """Verify G transforms match the physical mirror semantics.""" + # Construct a known obs: front has +fx, +fy; top has +fx, +fy; bot has +fx, +fy + # sensors: s0(top) ux=1, uy=2; s1 ux=3, uy=4; s2(bot) ux=5, uy=6 + obs = np.array([1, 2, 3, 4, 5, 6, # forces: front(1,2), top(3,4), bot(5,6) + 10, 20, 30, 40, 50, 60], dtype=np.float64) # sensors + g = apply_G_obs(obs) + # Expected: + # front_fx=1 (stays), front_fy=-2 (negated) + # top_fx <- bot_fx = 5, top_fy <- -bot_fy = -6 + # bot_fx <- top_fx = 3, bot_fy <- -top_fy = -4 + # s0_ux <- s2_ux = 50, s0_uy <- -s2_uy = -60 + # s1_ux = 30 (stays), s1_uy = -40 (negated) + # s2_ux <- s0_ux = 10, s2_uy <- -s0_uy = -20 + expected = np.array([1, -2, 5, -6, 3, -4, + 50, -60, 30, -40, 10, -20], dtype=np.float64) + assert np.allclose(g, expected), f"G_obs mismatch:\n got={g}\n exp={expected}" + print(" G_obs semantics: PASS") + + # Action: [aF, aT, aB] -> [-aF, -aB, -aT] + act = np.array([1.0, 2.0, 3.0]) + g_act = apply_G_action(act) + expected_act = np.array([-1.0, -3.0, -2.0]) + assert np.allclose(g_act, expected_act), f"G_action mismatch:\n got={g_act}\n exp={expected_act}" + print(" G_action semantics: PASS") + + +if __name__ == "__main__": + print("=== SymmetryAugmentWrapper verification ===") + _verify_g_involution() + _verify_symmetry_semantics() + print("\nAll checks passed. Wrapper is ready for use.") + print("\nUsage in train script:") + print(" env = KarmanCloakEnv(...)") + print(" env = SymmetryAugmentWrapper(env, prob=0.5)") + print(" vec_env = DummyVecEnv([lambda: env])") + print(" vec_env = VecNormalize(vec_env, ...)") diff --git a/src/drl_pinball/train/train_karman_2000x600.py b/src/drl_pinball/train/train_karman_2000x600.py new file mode 100644 index 0000000..98d5b96 --- /dev/null +++ b/src/drl_pinball/train/train_karman_2000x600.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +"""Train Karman Cloak (2000x600) — GPU-native, no device="cpu" hack. + +CUDA context management: + - PPO model on GPU (PyTorch owns the default context) + - CFD env wraps its calls in sim.ctx._ctx.push()/pop() (PyCUDA context) + - This mirrors the exact pattern in legacy env: push before CFD, pop after. + +Usage: + conda run -n pycuda_3_10 python -u train_karman_2000x600.py --device-id 0 --seed 42 --total-episodes 50 + + # Pre-record target to speed up: + python -c "from env_karman_2000x600 import record_target; import numpy as np; np.save('target.npy', record_target(0))" + conda run -n pycuda_3_10 python -u train_karman_2000x600.py --device-id 0 --seed 42 --target target.npy +""" +from __future__ import annotations + +import argparse, json, os, sys, time +from pathlib import Path + +import numpy as np +import pycuda.driver as cuda; cuda.init() + +_REPO = Path(__file__).resolve().parents[3] +if str(_REPO) not in sys.path: + sys.path.insert(0, str(_REPO)) + +import torch +from torch.nn import Module as TorchModule +from stable_baselines3 import PPO +from stable_baselines3.common.vec_env import DummyVecEnv, VecNormalize +from torch.utils.tensorboard import SummaryWriter + +from env_karman_2000x600 import KarmanCloakEnv, record_target +from symmetry_wrapper import SymmetryAugmentWrapper + + +class Sin(TorchModule): + def __init__(self): super().__init__() + def forward(self, x): return torch.sin(x) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Train Karman Cloak 2000x600") + parser.add_argument("--device-id", type=int, default=0) + parser.add_argument("--seed", type=int, default=1) + parser.add_argument("--total-episodes", type=int, default=500, + help="Total episodes (default 500)") + parser.add_argument("--n-steps", type=int, default=2048, + help="PPO rollout buffer size (default 2048 = legacy)") + parser.add_argument("--learn-timesteps", type=int, default=2048, + help="Timesteps per model.learn() call (should = n_steps)") + parser.add_argument("--batch-size", type=int, default=64) + parser.add_argument("--lr", type=float, default=3e-4) + parser.add_argument("--n-epochs", type=int, default=10, + help="PPO n_epochs (default 10 = legacy)") + parser.add_argument("--target", type=str, default=None) + parser.add_argument("--symmetry-prob", type=float, default=0.5, + help="Probability of G-symmetry augmentation (0=off, 0.5=half)") + parser.add_argument("--no-bias", action="store_true", + help="Train without action bias (ACTION_BIAS=[0,0,0], scale=12)") + args = parser.parse_args() + + bias_tag = "nobias" if args.no_bias else "bias" + run_name = f"{bias_tag}_seed{args.seed}_s{args.n_steps}_e{args.n_epochs}_v4" + out_dir = Path(__file__).resolve().parent / "output" / run_name + (out_dir / "models").mkdir(parents=True, exist_ok=True) + + log_path = out_dir / "train.log" + def log(msg): + line = f"[{time.strftime('%H:%M:%S')}] {msg}" + print(line, flush=True) + with open(log_path, "a") as f: f.write(line + "\n"); f.flush() + + writer = SummaryWriter(log_dir=str(out_dir / "tb")) + log(f"=== {run_name} ===") + log(f" Device={args.device_id}, Seed={args.seed}, lr={args.lr}, episodes={args.total_episodes}") + + if args.target: + target_states = np.load(args.target) + log(f" Loaded target from {args.target}") + else: + log(" Recording target...") + t0 = time.perf_counter() + target_states = record_target(args.device_id) + log(f" Target recorded in {time.perf_counter()-t0:.0f}s") + + log(" Creating env...") + t0 = time.perf_counter() + if args.no_bias: + action_bias = np.array([0.0, 0.0, 0.0], dtype=np.float32) + action_scale = 12.0 # wider range to cover Bias omega range [-12,12]×U0 + log(f" NoBias mode: scale={action_scale}, bias={action_bias}") + else: + action_bias = None # use default [0,-4,4] + action_scale = None # use default 8.0 + env = KarmanCloakEnv(device_id=args.device_id, seed=args.seed, + target_states=target_states, + action_bias=action_bias, + action_scale=action_scale) + # Symmetry augmentation: 50% prob G-mirror obs/action to encourage + # learning a symmetric (up/down) control law. + env = SymmetryAugmentWrapper(env, prob=args.symmetry_prob, seed=args.seed, + rollout_len=args.n_steps) + vec_env = DummyVecEnv([lambda: env]) + # VecNormalize: automatic running mean/std normalization for obs. + # norm_reward=False because reward is already in [0,1] and we want + # to preserve its magnitude for eval. clip_obs=10 is generous (physical + # norm already keeps obs in ~[-1,1] range). + vec_env = VecNormalize(vec_env, norm_obs=True, norm_reward=False, + clip_obs=10.0, gamma=0.99) + log(f" Env ready in {time.perf_counter()-t0:.0f}s (symmetry_prob={args.symmetry_prob}, VecNormalize)") + + # PPO on GPU — env handles CUDA context via push/pop around CFD calls + device = torch.device(f"cuda:{args.device_id}") + log(f" PPO device: {device}") + + model = PPO( + "MlpPolicy", + policy_kwargs={"activation_fn": Sin, "net_arch": [64, 64]}, + env=vec_env, + device=device, + n_steps=args.n_steps, + batch_size=args.batch_size, + n_epochs=args.n_epochs, + learning_rate=args.lr, + # Legacy-matching: use SB3 defaults (no ent_coef, no target_kl) + # ent_coef=0.0 (default), target_kl=None (default) + gamma=0.995, # keep higher gamma for normalized DTW delay propagation + verbose=0, + ) + log(" Created from scratch.") + + best_reward = -float("inf") + t_last = time.perf_counter() + + # Save VecNormalize statistics periodically for inference reuse + norm_path = str(out_dir / "vec_normalize.pkl") + + for ep in range(1, args.total_episodes + 1): + model.learn(total_timesteps=args.learn_timesteps, reset_num_timesteps=False) + + # --- Evaluation: disable symmetry augmentation for clean policy eval --- + eval_env = model.get_env() + # Access underlying env to turn off symmetry + try: + inner = eval_env.venv.envs[0] + if hasattr(inner, 'prob'): + inner.prob = 0.0 + except Exception: + pass + eval_obs = eval_env.reset() + ep_rewards = [] + ep_r_cd, ep_r_cl, ep_r_sim = [], [], [] + for _ in range(360): + action, _ = model.predict(eval_obs) # stochastic eval (matches legacy, smoother curves) + eval_obs, reward, done, info = eval_env.step(action) + ep_rewards.append(float(reward[0])) + inf = info[0] if isinstance(info, list) else info + if "r_cd" in inf: + ep_r_cd.append(float(inf["r_cd"])) + ep_r_cl.append(float(inf["r_cl"])) + ep_r_sim.append(float(inf["r_sim"])) + if done[0]: break + # Re-enable symmetry for next training round + try: + inner = eval_env.venv.envs[0] + if hasattr(inner, 'prob'): + inner.prob = args.symmetry_prob + except Exception: + pass + avg_r = np.mean(ep_rewards[-180:]) if len(ep_rewards) >= 180 else np.mean(ep_rewards) + dt = time.perf_counter() - t_last; t_last = time.perf_counter() + + writer.add_scalar("eval/avg_reward", avg_r, ep) + if ep_r_cd: + writer.add_scalar("eval/r_cd", float(np.mean(ep_r_cd[-180:])), ep) + writer.add_scalar("eval/r_cl", float(np.mean(ep_r_cl[-180:])), ep) + writer.add_scalar("eval/r_sim", float(np.mean(ep_r_sim[-180:])), ep) + + if avg_r > best_reward: + best_reward = avg_r + model.save(str(out_dir / "models" / "best_model.zip")) + # Save normalizer alongside best model + vec_env.save(norm_path) + log(f" Ep {ep:3d}: reward={avg_r:.4f} (BEST, {dt:.0f}s/ep) ** " + f"r_cd={np.mean(ep_r_cd[-180:]):.3f} r_cl={np.mean(ep_r_cl[-180:]):.3f} " + f"r_sim={np.mean(ep_r_sim[-180:]):.3f}") + elif ep % 5 == 0: + log(f" Ep {ep:3d}: reward={avg_r:.4f} (best={best_reward:.4f}, {dt:.0f}s/ep)") + + if ep % 10 == 0: + model.save(str(out_dir / "models" / f"chkpt_ep{ep}.zip")) + vec_env.save(norm_path) + + model.save(str(out_dir / "models" / "final_model.zip")) + vec_env.save(norm_path) + + meta = {"seed": args.seed, "total_episodes": args.total_episodes, + "best_reward": float(best_reward), "n_steps": args.n_steps, + "batch_size": args.batch_size, "n_epochs": args.n_epochs, "lr": args.lr} + with (out_dir / "meta.json").open("w") as f: + json.dump(meta, f, indent=2) + + env.close() + log(f"Done. Best reward: {best_reward:.4f}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/understanding_notes.md b/src/understanding_notes.md new file mode 100644 index 0000000..728d462 --- /dev/null +++ b/src/understanding_notes.md @@ -0,0 +1,399 @@ +# DynamisLab Understanding Notes + +## 1. 网格尺寸 (Grid Dimensions) + +### 旧版配置 +- `config_cuda.json`: X_1U=128, Y_1U=32, Z_1U=1 +- `config_flowfield.json`: field_dim_in_U=[10, 16, 1] +- 实际网格 = [128×10, 32×16, 1×1] = **[1280, 512, 1]** + +### 新版配置 +- `config_lbm_pinball.json`: nx=1280, ny=512 → **完全相同** + +所以网格尺寸没有变化。L0=20 是基尺度,但网格的 U 单位 = (128, 32) 是为了对齐 CUDA SM 数。 + +--- + +## 2. Re 数定义 + +**关键:** 用户使用了两种 Re 定义: + +| 符号 | 长度尺度 | 公式 | 默认值 | +|------|---------|------|--------| +| `Re_D` | 单个圆柱直径 D=20 | U0·D/ν | 0.01×20/0.004 = **50** | +| `Re` (代码中写) | 2×D = 40 | U0·(2D)/ν | 0.01×40/0.004 = **100** | + +- Confirmation report 中的 `Re_D = 50` 对应代码中的 Re100 +- 代码内写 re100 系列模型 → 实际物理 Re_D=50 +- 上游扰流圆柱直径 = L0×1 = 20 → Re=U0×20/ν=50 (单直径) + +--- + +## 3. 旧 API vs 新 API 关键差异 + +### 3.1 力的物理含义 + +**旧 API (`FlowField.run(N, action)`):** +``` +obs[:] = 0 +for step in range(N): + memset(obs_gpu, 0) # 每步清零 obs_gpu + step_kernel(...) # 每步 force 通过 atomicAdd 累加进 obs_gpu + obs += obs_gpu (H2D copy) # 每步读取到 host +obs /= N # 除以步数 = 每步平均力 +``` +→ 旧 `obs` = **每步平均力** + +**新 API (`sim.run(N)` / `sim.stepper.step(N, ...)`):** +``` +# stepper 内部不会清零 obs +# 需要用户手动清零 +sim.bodies.zero_force_segment_async(stream) # 清零力/扭矩段 +sim.stepper.step(N, ..., stream) +sim.read_force(id) → 累积 N 步后的值 +``` +→ 新 `sim.read_force()` = **N 步累积力**(需除以 N 得到每步平均) + +**对应关系:** +- 旧 `obs` = 新 `sim.read_force(id)` ÷ N +- 旧 `obs[i]` 的 index 顺序 = 传感器在前 + 力在后 +- 新 API 分别用 `read_force(id)` 和 `read_sensor(id)` 分开读取 + +### 3.2 传感器的物理含义 + +**旧 API:** +- Sensor 值通过 atomicAdd 累加,跟力一样每步清零、host 累积、除以步数 +- 所以旧 `obs` 中的 sensor = **每步平均速度** + +**新 API:** +- SensorKernel 用 atomicAdd 累加 `ux, uy` 到 `obs_gpu` 的 sensor 段 +- `read_sensor(id, normalize=True)` 除以 `sensor_cell_counts[body_id]` (该 sensor 覆盖的格子数) +- **但没有除以步数!** + +```python +# 新 API 读取 sensor: +# read_sensor(id) = (sum_{steps} sum_{cells} ux) / cell_count +# 要得到每步平均 = read_sensor(id) / N +``` + +**所以旧 → 新转换:** +```python +# 旧环境: +# flow_field.run(SAMPLE_INTERVAL, action) +# obs = flow_field.obs # 已经是每步平均 + +# 新环境等效: +# sim.bodies.zero_force_segment_async(stream) +# sim.bodies.zero_sensor_segment_async(stream) +# sim.run(SAMPLE_INTERVAL) # 或 stepper.step(SAMPLE_INTERVAL) +# fx_per_step = sim.read_force(id)[0] / SAMPLE_INTERVAL +# fy_per_step = sim.read_force(id)[1] / SAMPLE_INTERVAL +# ux_per_step = sim.read_sensor(id)[0] / SAMPLE_INTERVAL +# uy_per_step = sim.read_sensor(id)[1] / SAMPLE_INTERVAL +``` + +### 3.3 Obs 布局对比 + +**旧 API obs 数组(以 cloak env 为例,7 个 objects):** +``` +obs[0:6] = sensor0_ux, sensor0_uy, sensor1_ux, sensor1_uy, sensor2_ux, sensor2_uy +obs[6:12] = cylinder0_fx, cylinder0_fy, cylinder1_fx, cylinder1_fy, cylinder2_fx, cylinder2_fy +obs[12:14] = dist_cylinder_fx, dist_cylinder_fy (扰流圆柱) +``` +objects 添加顺序:传感器 → 圆柱体。obs 是先 sensor 段(6个值),后 cylinder 段(每个 cyL 2 个值)。 + +**新 API:** +- `sim.read_force(body_id)` → `[fx, fy]` 按 body_id 读取 +- `sim.read_sensor(body_id)` → `[ux, uy]` 按 body_id 读取 +- `sim.read_torque(body_id)` → `[tz]` 按 body_id 读取 + +### 3.4 Checkpoint/恢复 + +**旧 API:** +```python +flow_field.get_ddf() # host ← gpu +flow_field.save_ddf() # 保存到 host 内存 +flow_field.restore_ddf() # host 内存恢复 +flow_field.apply_ddf() # host → gpu +``` + +**新 API:** +```python +sim.snapshot() # 内存快照 +sim.restore() # 恢复 +# 或 +sim.save_checkpoint(path) # HDF5 文件 +sim.load_checkpoint(path) # 从文件恢复 +``` + +### 3.5 动作施加方式 + +**旧 API:** +```python +# 全部 action 打包成一个数组,按 object 顺序排列 +action_array = np.zeros(n_objects, dtype=DATA_TYPE) +action_array[4:7] = ... # 圆柱体的 omega 值 +flow_field.run(N, action_array) +# 内部有指数平滑:action_pinned = (1-weight)*action_pinned + weight*action_target +``` + +**新 API:** +```python +sim.set_body(body_id, omega=value) # 每个圆柱体单独设置 +sim.run(N) +# 没有内置动作平滑(通过 ActionSmoother 可以实现) +``` + +### 3.6 数值错误检测 + +**旧 API:** +```python +flow_field.has_numeric_error() +flow_field.last_error_flag +``` + +**新 API:** +需要手动实现检查。 + +--- + +## 4. 五种场景详解 + +### 4.1 Karman Cloak (模型名: d1a3o12_re系列) + +**目标:** 上游 2D 扰流圆柱产生涡街,控制 pinball 使下游传感器信号跟无 pinball 时一致 + +**几何布局:** +``` +上游扰流圆柱:x=10*L0, y=中心, r=L0 (=200, 240, 20) +Pinball前: x=30*L0, y=中心, r=L0/2 (=600, 240, 10) +Pinball下: x=31.3*L0, y=中心-0.75*L0, r=L0/2 (=626, 225, 10) +Pinball上: x=31.3*L0, y=中心+0.75*L0, r=L0/2 (=626, 255, 10) +Sensor上: x=40*L0, y=中心+2*L0, r=L0/4 (=800, 280, 5) +Sensor中: x=40*L0, y=中心, r=L0/4 (=800, 240, 5) +Sensor下: x=40*L0, y=中心-2*L0, r=L0/4 (=800, 200, 5) +``` + +**目标信号:** 只有上游扰流圆柱(无 pinball)时,sensor 的时间序列 +**观测:** `forces[6]+sens[6]` = 6 个 force 值 + 6 个 sensor 值 = 12 维 +**动作缩放:** `action×8U₀ + [0, -4U₀, 4U₀]` (=0,0 代表 -4,4 的偏置动作) +**Reward:** `min(0.3×exp(-|Cd×20|) + 0.4×exp(-|Cl×80|) + 0.3×exp(-10×|sim-1|), 1.0)` +**SAMPLE_INTERVAL:** 800 +**CONV_LEN:** 30 +**检查点流程:** +1. 初始化只有扰流圆柱+3传感器的环境 → 运行稳定 → 记录150步目标信号 +2. 添加 pinball → 稳定 → 保存 DDF(无控制状态) +3. 用零动作运行 150 步 → 记录 obs 到 fifo → 计算 norm +4. 用预设动作(-4,4)运行 150 步 → 记录 obs → 保存 save_states +5. reset=恢复DDF + 恢复fifo + +### 4.2 Erase (模型名: d1a3o12_250729_250326_erase系列) + +**目标:** 跟 Karman Cloak 场景基本一致,但目标是让下游流场恢复到"入口流状态"(干净来流),即抹除扰流圆柱尾迹 + +**关键区别:** +- 目标信号 = 入口均匀流的均值(基本为零方差) +- Reward 主要看传感器与均值的偏差 +- **这是未完全实现的部分** +- 动作缩放: `×8U₀ + [0, -8U₀, 8U₀]` + +### 4.3 Illusion (模型名: d1a3o14_250525_imit系列) + +**目标:** 上游干净来流,控制 pinball 使其下游流场跟指定直径的单个圆柱一致 + +**几何差异:** +- 没有上游扰流圆柱 +- 目标圆柱位置不同(x=20*L0 或 x=31*L0,取决于场景) +- Sensor 位置可能调整(x=30*L0 而不是 40×L0,为了贴近目标圆柱等效距离) +- 目标信号通过 harmonics 分析(FFT 提取频域特征)进行重构 + +**观测:** 12 维 (forces) + 2 维 (target_cd, target_cl) = 14 维 +**Reward:** `min(0.3×exp(-|(Cd-Cd_target)×10|) + 0.3×exp(-|(Cl-Cl_target)×10|) + 0.4×exp(-10×|sim-1|), 1.0)` +- 力的比较是和目标圆柱的谐波重构值比较 + +**IMPORTANT CORRECTION (2026-06-12):** "2U" in model name means S_DIM=14 (2 extra target force dimensions), NOT 2x velocity. ALL models train with u0=0.01. SAMPLE_INTERVAL varies by diameter: +- 0.75L: `..._075L_2U_400S` → S=400 +- 1.0L: `..._1L_2U_600S` → S=600 +- 1.5L: `..._15L_2U` → no S suffix = default S=800 +- nu=0.004 for all (no Vis suffix = default) — confirmed via sweep + +### 4.4 Vortex (模型名: vortex_lamb / vortex_taylor) + +**目标:** 在初始流场中加入涡量(Lamb dipole 或 Taylor vortex),控制 pinball 使下游信号跟无 pinball 时一致 + +**关键特征:** +- 要正确使用 `add_vortex` 进行初始化 +- Dipole (Lamb) 强度较大 (0.5*U0),Monopole (Taylor) 强度较小 (0.03*U0) +- Vortex 在保存 DDF 之后添加,随流场演化 +- 有 `MAX_STEPS=150` 的终止条件(因为是 transient 事件) +- 动作缩放: `×4U₀ + [0, -4U₀, 4U₀]` +- Reward: `min(0.2×exp(-|Cd×20|) + 0.3×exp(-|Cl×80|) + 0.5×exp(-10×|sim-1|), 1.0)` + +### 4.5 Reduced Obs (模型名: d1a3o12_250421系列) + +**目标:** 同 Karman Cloak,但逐步减少观测维度 +**观测:** 从 12 → 9 → 5 → 3 → 2 递减,观察模型是否能适应 + +--- + +## 5. Norm 采集过程(关键!) + +**所有旧环境共有的模式:** + +```python +# 环境初始化最后阶段: + +# Step 1: 用零动作运行 FIFO_LEN 步,记录 obs +for i in range(FIFO_LEN): + flow_field.run(SAMPLE_INTERVAL, zero_action) + fifo_states.append(flow_field.obs[sensor_select]) + +# Step 2: 从 fifo 计算 norm +temp_states = np.array(fifo_states) +force_norm_fact = 6 * max(|forces|) # 力归一化因子 +for i in range(6): + sens_deviation[i] = mean(sensor[i]) # 均值 + sens_norm_fact[i] = 5 * max(|sensor[i] - mean|) # 波动范围×5 + +# Step 3: 恢复 DDF,用预设动作运行 FIFO_LEN 步 +flow_field.apply_ddf() +for i in range(FIFO_LEN): + flow_field.run(SAMPLE_INTERVAL, init_action) + fifo_states.append(...) +save_states = fifo_states.copy() +``` + +这个 norm 值是**模型训练时就固化**的,**推理时也必须使用完全相同的值**。旧版环境在初始化时自动计算,新环境需要为每个场景预计算和存储这些 norm 值。 + +--- + +## 6. uni_test.ipynb 流程解析 (最重要参考) + +这个 notebook 是一个连续的、视频式展示,包含以下序列: + +### Phase 0: 初始化和元数据采集 +1. 加载所有模型 +2. 创建 DummyEnv 用于加载模型(只是一个匹配 obs/action space 的空壳) +3. 设置 CUDA context,创建 `FlowField` +4. 设置 `meta_*` 对象存储 norm 值 + +### Phase 1: 目标信号录制 +1. **Steady 目标**:3 sensors × 150步 → 记录时序均值(干净来流) +2. **Dipole (Lamb)** 目标:restore DDF → add_vortex Lamb → 150步 → 记录 +3. **Monopole (Taylor)** 目标:restore DDF → add_vortex Taylor → 150步 → 记录 +4. **Illusion 目标**(1.0L 圆柱):新 FlowField → 单圆柱 x=31*L0, r=1*L0 → 3 sensors → 150步 × 800 采样 → 记录 8 维 obs + harmonics 分析 +5. **Illusion 目标**(0.75L 圆柱):类似,SAMPLE_INTERVAL=400 +6. **Illusion 目标**(1.5L 圆柱):类似,SAMPLE_INTERVAL=800 +7. **Karman 目标信号**:pinball 恢复 + 扰流圆柱 → 3 sensors → 150步 × 800 采样 → 记录 sensor+force obs + +### Phase 2: Pinball + Norm 采集 +- 创建 pinball → 运行到稳定 → save DDF +- 零动作 150步 → 计算 norm +- 预设动作 150步 → 记录 save_states + +### Phase 3: Norm 采集(各个场景) +- 跟 Phase 2 类似,但针对各个特定场景恢复 DDF 后分别采集 norm + +### Phase 4: 推理运行(控制演示序列) +1. **No Control (nc)** 基线:加载 pinball DDF → 零动作 100步 × 1000采样 → 保存场 +2. **Steady Cloaking**: 从 nc 继续 → 逐步切换到 cloak 动作(75步渐入+保持) +3. **Dipole Cloaking**: restore DDF → add Lamb vortex → 用 cloak_lamb 模型推理 → 25步渐入/25步淡出 → 保存场 +4. **Monopole Cloaking**: restore DDF → add Taylor vortex → 用 cloak_taylor 模型推理 → 类似渐入淡出 +5. **Illusion (1L)**: restore DDF → 用 illusion_1L 模型推理 → 10步渐入 → 200步保持 +6. **Illusion (0.75L)**: 类似 +7. **Illusion (1.5L)**: 类似 +8. **Karman NC + Cloak**: restore Karman场景DDF → 200步无控制 → 切换到 cloak_re100 模型推理 +9. **Frame 合成 → ffmpeg 视频** + +关键点: +- **场景间过渡**通过 `restore/apply DDF` 实现 +- **动作渐入/淡出**用 `linear interpolation in action space` +- 场数据存在 `.dat` 文件(Tecplot格式),用 `save_field()` 保存 +- 所有推理都用 `deterministic=True` + +--- + +## 7. 旧 API 动作数组的索引规则 + +旧 `FlowField.run()` 的 `action_target` 数组按 object 添加顺序排列: + +``` +| Sensors 的 action slot | Cylinders 的 action slot | +| sensor0, sensor1, ... | cylinder0, cylinder1, ... | +``` + +每个 object 的 action slot 有 3×DIM 个值(DIM=2 时=6),但 LBM 中 sensor 只用第一个值(实际上被忽略),cylinder 用最后一个值(omega)。 + +``` +action[0] = sensor0 (ignored) +action[1] = sensor0 (ignored) +action[2] = sensor0 (ignored) +action[3] = sensor1 (ignored) +action[4] = sensor1 (ignored) ← 对于 cloak env,这里放 cylinder0 的 omega +action[5] = sensor1 (ignored) +action[6] = cylinder0 omega (disturbance cylinder for cloak) +action[7] = cylinder1 omega (front pinball) ← 注意!对象索引顺序取决于添加顺序 +``` + +实际各环境设置 action 的方式: +```python +# cloak env (7 objects: 3 sensors + 1 dist_cyl + 3 pinball): +temp = np.zeros(7, dtype=DATA_TYPE) +temp[4:7] = (action*8 + [0,-4,4]) * U0 # action[4]=front omega, [5]=bottom, [6]=top + +# erase env: +temp[4:7] = (action*8 + [0,-8,8]) * U0 + +# imit env (6 objects: 3 sensors + 3 pinball): +temp[3:6] = (action*8 + [0,-2,2]) * U0 + +# vortex env: +temp[3:6] = (action*4 + [0,-4,4]) * U0 +``` + +注意 `temp[4:7]` 是从 index 4 开始取 3 个值。这是因为添加顺序是: +1. sensor0 (id=0) +2. sensor1 (id=1) +3. sensor2 (id=2) +4. dist_cylinder (id=3) — 旧 env 里添加上游扰流圆柱后固定为 0 +5. pinball_front (id=4) +6. pinball_bottom (id=5) +7. pinball_top (id=6) + +--- + +## 8. 重写注意事项 + +### 8.1 力和传感器的归一化 +因为新 API 返回的是 N 步累积值(需要除以步数),所以 norm 的物理含义需要重新标定。 + +### 8.2 动作缩放关系 +旧环境动作缩放矩阵(从归一化 [-1,1] action 到物理 U0 倍数的 omega): + +| 场景 | 公式 (θ=action) | 物理范围 | +|------|----------------|---------| +| Cloak | `action×8 + [0,-4,4]` | front: [-8,8], bottom:[-12,4], top:[-4,12] | +| Erase | `action×8 + [0,-8,8]` | front: [-8,8], bottom:[-16,0], top:[0,16] | +| Illusion | `action×8 + [0,-2,2]` | front: [-8,8], bottom:[-10,6], top:[-6,10] | +| Vortex | `action×4 + [0,-4,4]` | front: [-4,4], bottom:[-8,0], top:[0,8] | + +然后乘以 U0=0.01 得到实际的晶格 omega 值。 + +### 8.3 边界条件 +旧配置是 parabolic inlet + no-slip walls(因为 legacy solver 不支持 free-slip) +新 pinball 配置是 parabolic inlet + **bounce_back** walls(即 no-slip,跟旧版一致) +注意新配置 `config_lbm_pinball.json` 的 `y_wall_bc` 是 `bounce_back`,验证配置 `run_kan99b` 是 `free_slip`,要统一。 + +### 8.4 重写策略 +1. **base_env.py**: 封装共享逻辑(Simulation 创建、几何布局、checkpoint 管理、norm 存储、DTW 工具) +2. **每个场景继承 base_env**: 只覆盖 reward、目标信号、动作缩放 +3. **train/*.py**: 跟旧版一致的 PPO 训练循环(Sin 激活、SB3 PPO、TensorBoard) +4. **eval/*.py**: 推理脚本,对应 uni_test 功能,用配置文件驱动场景序列 +5. **configs/**: 存储每个场景的 norm 值、几何参数、采样参数 +6. **models/**: 从旧版复制训练好的 .zip 模型(在新 API 推理中使用) + +### 8.5 与旧环境的数值一致性验证方法 +新环境在完全实现后,必须用旧训练好的模型在新 env 上跑推理,对比: +- 传感器时序信号(旧 `obs` vs 新 `read_sensor/SAMPLE_INTERVAL`) +- 力信号(旧 `obs` vs 新 `read_force/SAMPLE_INTERVAL`) +- 场数据(旧 `save_field` vs 新 `get_macroscopic`)