fix
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
package analize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/che4web/go4rest"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AnalizeController struct {
|
||||
*go4rest.ViewSet[Analize]
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewAnalizeController(db *gorm.DB) *AnalizeController {
|
||||
return &AnalizeController{
|
||||
ViewSet: go4rest.NewViewSet[Analize](db),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
type AnalizeSeriesPoint struct {
|
||||
CaseID uint `json:"case_id"`
|
||||
CaseName string `json:"case_name"`
|
||||
X float64 `json:"x"`
|
||||
Omega float64 `json:"omega"`
|
||||
PsiMax float64 `json:"psi_max"`
|
||||
}
|
||||
|
||||
type AnalizeSeriesResponse struct {
|
||||
Parameter string `json:"parameter"`
|
||||
Label string `json:"label"`
|
||||
Points []AnalizeSeriesPoint `json:"points"`
|
||||
}
|
||||
|
||||
var analizeParameterColumns = map[string]struct {
|
||||
Column string
|
||||
Label string
|
||||
}{
|
||||
"Rel": {Column: "rel", Label: "Rel"},
|
||||
"RelC": {Column: "rel_c", Label: "RelC"},
|
||||
"Le": {Column: "le", Label: "Le"},
|
||||
"Pr": {Column: "pr", Label: "Pr"},
|
||||
"Pe": {Column: "pe", Label: "Pe"},
|
||||
"Ma": {Column: "ma", Label: "Ma"},
|
||||
"Time": {Column: "time", Label: "Time"},
|
||||
}
|
||||
|
||||
func (c *AnalizeController) Series(ctx *gin.Context) {
|
||||
param := ctx.DefaultQuery("parameter", "Rel")
|
||||
selected, ok := analizeParameterColumns[param]
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid parameter"})
|
||||
return
|
||||
}
|
||||
|
||||
filterParam := ctx.Query("filter_parameter")
|
||||
filterSelected, filterOK := analizeParameterColumns[filterParam]
|
||||
filterMin := ctx.Query("filter_min")
|
||||
filterMax := ctx.Query("filter_max")
|
||||
|
||||
type row struct {
|
||||
CaseID uint `gorm:"column:case_id"`
|
||||
CaseName string `gorm:"column:case_name"`
|
||||
X float64 `gorm:"column:x"`
|
||||
Omega float64 `gorm:"column:omega"`
|
||||
PsiMax float64 `gorm:"column:psi_max"`
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
SELECT a.case_id, a.case_name, p.%s AS x, a.omega, a.psi_max
|
||||
FROM analizes a
|
||||
JOIN control_cases c ON c.id = a.case_id
|
||||
JOIN params p ON p.id = c.params_id
|
||||
`, selected.Column)
|
||||
where := make([]string, 0, 2)
|
||||
args := make([]any, 0, 2)
|
||||
if filterParam != "" {
|
||||
if !filterOK {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid filter parameter"})
|
||||
return
|
||||
}
|
||||
if filterMin != "" {
|
||||
where = append(where, fmt.Sprintf("p.%s >= ?", filterSelected.Column))
|
||||
args = append(args, filterMin)
|
||||
}
|
||||
if filterMax != "" {
|
||||
where = append(where, fmt.Sprintf("p.%s <= ?", filterSelected.Column))
|
||||
args = append(args, filterMax)
|
||||
}
|
||||
}
|
||||
if len(where) > 0 {
|
||||
query += " WHERE " + strings.Join(where, " AND ")
|
||||
}
|
||||
query += fmt.Sprintf(" ORDER BY p.%s, a.id", selected.Column)
|
||||
|
||||
var rows []row
|
||||
if err := c.db.Raw(query, args...).Scan(&rows).Error; err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
points := make([]AnalizeSeriesPoint, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
points = append(points, AnalizeSeriesPoint{
|
||||
CaseID: r.CaseID,
|
||||
CaseName: r.CaseName,
|
||||
X: r.X,
|
||||
Omega: r.Omega,
|
||||
PsiMax: r.PsiMax,
|
||||
})
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, AnalizeSeriesResponse{
|
||||
Parameter: param,
|
||||
Label: selected.Label,
|
||||
Points: points,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *AnalizeController) Recalculate(ctx *gin.Context) {
|
||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var item Analize
|
||||
if err := c.db.First(&item, id).Error; err != nil {
|
||||
ctx.JSON(http.StatusNotFound, gin.H{"error": "record not found"})
|
||||
return
|
||||
}
|
||||
|
||||
csvPath := filepath.Join(".", fmt.Sprintf("%d", item.CaseID), "foo.csv")
|
||||
updated, err := AnalyzeCSV(c.db, item.CaseID, item.CaseName, csvPath)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, updated)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package analize
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type Analize struct {
|
||||
gorm.Model
|
||||
Name string `json:"name"`
|
||||
CaseID uint `json:"case_id"`
|
||||
CaseName string `json:"case_name"`
|
||||
PsiMax float64 `json:"psi_max"`
|
||||
PsiLMax float64 `json:"psi_l_max"`
|
||||
Omega float64 `json:"omega"`
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package analize
|
||||
|
||||
import (
|
||||
"github.com/che4web/go4rest"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func RegisterApp(r *gin.Engine, db *gorm.DB) {
|
||||
db.AutoMigrate(&Analize{})
|
||||
|
||||
controller := NewAnalizeController(db)
|
||||
go4rest.RegisterCRUDRoutes(r, "analize", controller)
|
||||
r.GET("/api/analize/series", controller.Series)
|
||||
r.POST("/api/analize/:id/recalculate", controller.Recalculate)
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package analize
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gonum.org/v1/gonum/dsp/fourier"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func AnalyzeCSV(db *gorm.DB, caseID uint, caseName, csvPath string) (*Analize, error) {
|
||||
file, err := os.Open(csvPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
reader := csv.NewReader(file)
|
||||
records, err := reader.ReadAll()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(records) < 2 {
|
||||
return nil, fmt.Errorf("csv has no data rows")
|
||||
}
|
||||
|
||||
headers := make(map[string]int, len(records[0]))
|
||||
for i, header := range records[0] {
|
||||
headers[strings.ToLower(strings.TrimSpace(header))] = i
|
||||
}
|
||||
|
||||
getIndex := func(name string) (int, error) {
|
||||
idx, ok := headers[strings.ToLower(name)]
|
||||
if !ok {
|
||||
return -1, fmt.Errorf("missing column %q", name)
|
||||
}
|
||||
return idx, nil
|
||||
}
|
||||
|
||||
tIndex, err := getIndex("t")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
psiMIndex, err := getIndex("psi_m")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
psiLIndex, err := getIndex("psi_l")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
times := make([]float64, 0, len(records)-1)
|
||||
psiM := make([]float64, 0, len(records)-1)
|
||||
psiL := make([]float64, 0, len(records)-1)
|
||||
|
||||
for _, record := range records[1:] {
|
||||
if len(record) <= maxInt(tIndex, psiMIndex, psiLIndex) {
|
||||
continue
|
||||
}
|
||||
|
||||
t, err := parseFloat(record[tIndex])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
m, err := parseFloat(record[psiMIndex])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
l, err := parseFloat(record[psiLIndex])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
times = append(times, t)
|
||||
psiM = append(psiM, m)
|
||||
psiL = append(psiL, l)
|
||||
}
|
||||
|
||||
if len(psiL) == 0 {
|
||||
return nil, fmt.Errorf("no numeric rows found in csv")
|
||||
}
|
||||
|
||||
psiMax := maxFloat(psiM)
|
||||
psiLMax := maxFloat(psiL)
|
||||
omega := mainAngularFrequency(times, psiL)
|
||||
|
||||
result := &Analize{
|
||||
Name: fmt.Sprintf("analysis-%d", caseID),
|
||||
CaseID: caseID,
|
||||
CaseName: caseName,
|
||||
PsiMax: psiMax,
|
||||
PsiLMax: psiLMax,
|
||||
Omega: omega,
|
||||
}
|
||||
|
||||
var existing Analize
|
||||
err = db.Where("case_id = ?", caseID).First(&existing).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if err := db.Create(result).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result.ID = existing.ID
|
||||
if err := db.Save(result).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func parseFloat(value string) (float64, error) {
|
||||
return strconv.ParseFloat(strings.TrimSpace(value), 64)
|
||||
}
|
||||
|
||||
func maxInt(values ...int) int {
|
||||
max := 0
|
||||
for _, v := range values {
|
||||
if v > max {
|
||||
max = v
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
func maxFloat(values []float64) float64 {
|
||||
if len(values) == 0 {
|
||||
return 0
|
||||
}
|
||||
max := values[0]
|
||||
for _, v := range values[1:] {
|
||||
if v > max {
|
||||
max = v
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
func mainAngularFrequency(times, values []float64) float64 {
|
||||
if len(values) < 2 || len(times) < 2 {
|
||||
return 0
|
||||
}
|
||||
|
||||
dt := averageDelta(times)
|
||||
if dt <= 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
centered := make([]float64, len(values))
|
||||
mean := 0.0
|
||||
for _, v := range values {
|
||||
mean += v
|
||||
}
|
||||
mean /= float64(len(values))
|
||||
for i, v := range values {
|
||||
centered[i] = v - mean
|
||||
}
|
||||
|
||||
fft := fourier.NewFFT(len(centered))
|
||||
coeffs := fft.Coefficients(nil, centered)
|
||||
if len(coeffs) < 2 {
|
||||
return 0
|
||||
}
|
||||
|
||||
bestIndex := 1
|
||||
bestAmp := 0.0
|
||||
for i := 1; i < len(coeffs); i++ {
|
||||
amp := absComplex(coeffs[i])
|
||||
if amp > bestAmp {
|
||||
bestAmp = amp
|
||||
bestIndex = i
|
||||
}
|
||||
}
|
||||
|
||||
frequencyHz := fft.Freq(bestIndex) / dt
|
||||
return 2 * math.Pi * frequencyHz
|
||||
}
|
||||
|
||||
func absComplex(v complex128) float64 {
|
||||
return math.Hypot(real(v), imag(v))
|
||||
}
|
||||
|
||||
func averageDelta(times []float64) float64 {
|
||||
if len(times) < 2 {
|
||||
return 0
|
||||
}
|
||||
total := 0.0
|
||||
count := 0
|
||||
for i := 1; i < len(times); i++ {
|
||||
delta := times[i] - times[i-1]
|
||||
if delta <= 0 {
|
||||
continue
|
||||
}
|
||||
total += delta
|
||||
count++
|
||||
}
|
||||
if count == 0 {
|
||||
return 0
|
||||
}
|
||||
return total / float64(count)
|
||||
}
|
||||
Reference in New Issue
Block a user