package control_case import ( "encoding/csv" "fmt" "math" "net/http" "os" "path/filepath" "strconv" "strings" "github.com/che4web/go4rest" "github.com/gin-gonic/gin" "gonum.org/v1/gonum/dsp/fourier" "gorm.io/gorm" ) type ControlCaseController struct { *go4rest.ViewSet[ControlCase] db *gorm.DB } func NewControlCaseController(db *gorm.DB) *ControlCaseController { viewSet := go4rest.NewViewSet[ControlCase](db) viewSet.PreloadField = []string{"Params", "Params.InitialCondition"} return &ControlCaseController{ ViewSet: viewSet, db: db, } } type ParamsController struct { *go4rest.ViewSet[Params] db *gorm.DB } type InitialConditionController struct { *go4rest.ViewSet[InitialCondition] db *gorm.DB } func NewParamsController(db *gorm.DB) *ParamsController { viewSet := go4rest.NewViewSet[Params](db) viewSet.PreloadField = []string{"InitialCondition"} return &ParamsController{ ViewSet: viewSet, db: db, } } func NewInitialConditionController(db *gorm.DB) *InitialConditionController { return &InitialConditionController{ ViewSet: go4rest.NewViewSet[InitialCondition](db), db: db, } } type LaunchControlCaseRequest struct { ParamsID uint `json:"params_id" binding:"required"` Name string `json:"name"` } func (c *ControlCaseController) Launch(ctx *gin.Context) { var req LaunchControlCaseRequest if err := ctx.ShouldBindJSON(&req); err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var params Params if err := c.db.First(¶ms, req.ParamsID).Error; err != nil { ctx.JSON(http.StatusNotFound, gin.H{"error": "params not found"}) return } name := req.Name if strings.TrimSpace(name) == "" { name = fmt.Sprintf("case-%d", params.ID) } controlCase := ControlCase{ Name: name, ParamsID: params.ID, Status: "N", } if err := c.db.Create(&controlCase).Error; err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if err := c.db.Preload("Params").Preload("Params.InitialCondition").First(&controlCase, controlCase.ID).Error; err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } ctx.JSON(http.StatusCreated, controlCase) } type CSVSeriesResponse struct { Columns []string `json:"columns"` Rows [][]interface{} `json:"rows"` } type FFTPoint struct { Frequency float64 `json:"frequency"` Amplitude float64 `json:"amplitude"` } type PSISpectrumResponse struct { TimeStep float64 `json:"time_step"` Points map[string][]FFTPoint `json:"points"` } type InitialConditionResponse struct { InitialCondition InitialCondition `json:"initial_condition"` } func (c *ControlCaseController) ChartData(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 controlCase ControlCase if err := c.db.Preload("Params").First(&controlCase, id).Error; err != nil { ctx.JSON(http.StatusNotFound, gin.H{"error": "record not found"}) return } csvPath := controlCase.FooCSVPath() file, err := os.Open(csvPath) if err != nil { ctx.JSON(http.StatusNotFound, gin.H{"error": "csv file not found"}) return } defer file.Close() reader := csv.NewReader(file) records, err := reader.ReadAll() if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if len(records) == 0 { ctx.JSON(http.StatusOK, CSVSeriesResponse{Columns: []string{}, Rows: [][]interface{}{}}) return } columns := records[0] rows := make([][]interface{}, 0, len(records)-1) for _, record := range records[1:] { row := make([]interface{}, 0, len(record)) for _, value := range record { trimmed := strings.TrimSpace(value) if f, err := strconv.ParseFloat(trimmed, 64); err == nil { row = append(row, f) } else { row = append(row, trimmed) } } rows = append(rows, row) } ctx.JSON(http.StatusOK, CSVSeriesResponse{Columns: columns, Rows: rows}) } func (c *ControlCaseController) PSISpectrum(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 controlCase ControlCase if err := c.db.Preload("Params").First(&controlCase, id).Error; err != nil { ctx.JSON(http.StatusNotFound, gin.H{"error": "record not found"}) return } csvPath := controlCase.FooCSVPath() file, err := os.Open(csvPath) if err != nil { ctx.JSON(http.StatusNotFound, gin.H{"error": "csv file not found"}) return } defer file.Close() reader := csv.NewReader(file) records, err := reader.ReadAll() if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if len(records) < 2 { ctx.JSON(http.StatusOK, PSISpectrumResponse{Points: map[string][]FFTPoint{}}) return } 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 { ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } psiMIndex, err := getIndex("psi_m") if err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } psiLIndex, err := getIndex("psi_l") if err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } 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) } timeStep := averageDelta(times) if timeStep <= 0 { ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid time step"}) return } ctx.JSON(http.StatusOK, PSISpectrumResponse{ TimeStep: timeStep, Points: map[string][]FFTPoint{ "psi_m": buildSpectrumPoints(psiM, timeStep), "psi_l": buildSpectrumPoints(psiL, timeStep), }, }) } func buildSpectrumPoints(values []float64, dt float64) []FFTPoint { if len(values) < 2 || dt <= 0 { return []FFTPoint{} } 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) limit := len(coeffs) / 2 points := make([]FFTPoint, 0, limit+1) for i := 0; i <= limit; i++ { points = append(points, FFTPoint{ Frequency: float64(i) / (float64(len(centered)) * dt), Amplitude: math.Hypot(real(coeffs[i]), imag(coeffs[i])) / float64(len(centered)), }) } return points } 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 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) } func (c *ControlCaseController) FieldMap(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 } timeValue, err := strconv.ParseFloat(ctx.Query("time"), 64) if err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid time"}) return } var controlCase ControlCase if err := c.db.First(&controlCase, id).Error; err != nil { ctx.JSON(http.StatusNotFound, gin.H{"error": "record not found"}) return } result, err := ReadNearestFieldMap(controlCase.StorageH5Path(), timeValue) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } ctx.JSON(http.StatusOK, result) } func (c *ControlCaseController) CreateInitialCondition(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 controlCase ControlCase if err := c.db.First(&controlCase, id).Error; err != nil { ctx.JSON(http.StatusNotFound, gin.H{"error": "record not found"}) return } outputPath := filepath.Join(BASE_DIR, "initial_conditions", fmt.Sprintf("control_case_%d", controlCase.ID), "storage.h5") if err := controlCase.CreateStorageH5FromLastState(outputPath); err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } initialCondition := InitialCondition{ Name: fmt.Sprintf("control-case-%d", controlCase.ID), FilePath: outputPath, } if err := c.db.Where("file_path = ?", outputPath).Assign(initialCondition).FirstOrCreate(&initialCondition).Error; err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } ctx.JSON(http.StatusOK, InitialConditionResponse{InitialCondition: initialCondition}) }