393 lines
9.4 KiB
Go
393 lines
9.4 KiB
Go
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})
|
|
}
|