This commit is contained in:
che
2026-07-11 16:13:07 +05:00
parent 111cadd184
commit b815cde04c
24 changed files with 2858 additions and 76 deletions
+162 -1
View File
@@ -1,7 +1,17 @@
package control_case
import (
"encoding/csv"
"fmt"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"control/analize"
"github.com/che4web/go4rest"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
@@ -11,8 +21,11 @@ type ControlCaseController struct {
}
func NewControlCaseController(db *gorm.DB) *ControlCaseController {
viewSet := go4rest.NewViewSet[ControlCase](db)
viewSet.PreloadField = []string{"Params"}
return &ControlCaseController{
ViewSet: go4rest.NewViewSet[ControlCase](db),
ViewSet: viewSet,
db: db,
}
}
@@ -28,3 +41,151 @@ func NewParamsController(db *gorm.DB) *ParamsController {
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(&params, 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").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"`
}
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 := filepath.Join(".", strconv.FormatUint(uint64(controlCase.ParamsID), 10), "foo.csv")
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) RecalculateAnalysis(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
}
csvPath := filepath.Join(".", strconv.FormatUint(uint64(controlCase.ParamsID), 10), "foo.csv")
result, err := analize.AnalyzeCSV(c.db, controlCase.ID, controlCase.Name, csvPath)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusOK, result)
}
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
}
h5Path := filepath.Join(".", strconv.FormatUint(uint64(controlCase.ParamsID), 10), "storage.h5")
result, err := ReadNearestFieldMap(h5Path, timeValue)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusOK, result)
}
+239
View File
@@ -0,0 +1,239 @@
package control_case
import (
"fmt"
"os/exec"
"regexp"
"strconv"
"strings"
)
type FieldMapResponse struct {
RequestedT float64 `json:"requested_t"`
StageT float64 `json:"stage_t"`
Rows int `json:"rows"`
Cols int `json:"cols"`
Fields map[string][][]float64 `json:"fields"`
}
var (
stagePathRe = regexp.MustCompile(`^\s*group\s+/map/(stage_t=[^\s]+)\s*$`)
dimsRe = regexp.MustCompile(`\(\s*(\d+)\s*,\s*(\d+)\s*\)`)
floatInDataRe = regexp.MustCompile(`[-+]?(?:\d*\.\d+|\d+\.?\d*)(?:[eE][-+]?\d+)?`)
)
func ReadNearestFieldMap(filePath string, requestedT float64) (*FieldMapResponse, error) {
stageGroups, err := listStageGroups(filePath)
if err != nil {
return nil, err
}
if len(stageGroups) == 0 {
return nil, fmt.Errorf("no stage_t groups found")
}
selected := stageGroups[0]
for _, stage := range stageGroups[1:] {
if absFloat64(stage.stageT-requestedT) < absFloat64(selected.stageT-requestedT) {
selected = stage
}
}
result := &FieldMapResponse{
RequestedT: requestedT,
StageT: selected.stageT,
Fields: make(map[string][][]float64, 4),
}
for _, fieldName := range []string{"psi", "phi", "T", "C"} {
dims, values, err := readDatasetMatrix(filePath, selected.path, fieldName)
if err != nil {
return nil, err
}
if len(dims) != 2 {
return nil, fmt.Errorf("unexpected dims for %s: %v", fieldName, dims)
}
matrix, err := reshapeFloat64(values, dims[0], dims[1])
if err != nil {
return nil, fmt.Errorf("failed to reshape dataset %s: %w", fieldName, err)
}
matrix = transposeFloat64(matrix)
if result.Rows == 0 && result.Cols == 0 {
result.Rows = len(matrix)
if result.Rows > 0 {
result.Cols = len(matrix[0])
}
}
result.Fields[fieldName] = matrix
}
return result, nil
}
type stageFieldMap struct {
path string
stageT float64
}
func listStageGroups(filePath string) ([]stageFieldMap, error) {
out, err := runH5Dump(filePath, "-n")
if err != nil {
return nil, err
}
var stages []stageFieldMap
for _, line := range strings.Split(string(out), "\n") {
matches := stagePathRe.FindStringSubmatch(line)
if len(matches) != 2 {
continue
}
stageT, err := strconv.ParseFloat(strings.TrimPrefix(matches[1], "stage_t="), 64)
if err != nil {
continue
}
stages = append(stages, stageFieldMap{
path: "/map/" + matches[1],
stageT: stageT,
})
}
return stages, nil
}
func readDatasetMatrix(filePath, stagePath, fieldName string) ([]int, []float64, error) {
datasetPath := stagePath + "/" + fieldName
headerOut, err := runH5Dump(filePath, "-H", "-d", datasetPath)
if err != nil {
return nil, nil, err
}
rows, cols, err := parseDimsFromHeader(headerOut)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse dims for %s: %w", datasetPath, err)
}
dataOut, err := runH5Dump(filePath, "-d", datasetPath)
if err != nil {
return nil, nil, err
}
values, err := parseFloatData(dataOut)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse data for %s: %w", datasetPath, err)
}
if rows*cols != len(values) {
return nil, nil, fmt.Errorf("value count %d does not match %dx%d for %s", len(values), rows, cols, datasetPath)
}
return []int{rows, cols}, values, nil
}
func parseDimsFromHeader(out []byte) (int, int, error) {
matches := dimsRe.FindSubmatch(out)
if len(matches) != 3 {
return 0, 0, fmt.Errorf("dimensions not found")
}
rows, err := strconv.Atoi(string(matches[1]))
if err != nil {
return 0, 0, err
}
cols, err := strconv.Atoi(string(matches[2]))
if err != nil {
return 0, 0, err
}
return rows, cols, nil
}
func parseFloatData(out []byte) ([]float64, error) {
lines := strings.Split(string(out), "\n")
values := make([]float64, 0, 2048)
inData := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
switch {
case trimmed == "DATA {":
inData = true
continue
case inData && trimmed == "}":
return values, nil
case !inData:
continue
}
colon := strings.Index(trimmed, ":")
if colon < 0 {
continue
}
payload := strings.TrimSpace(trimmed[colon+1:])
if payload == "" {
continue
}
matches := floatInDataRe.FindAllString(payload, -1)
for _, match := range matches {
value, err := strconv.ParseFloat(match, 64)
if err != nil {
return nil, err
}
values = append(values, value)
}
}
if !inData {
return nil, fmt.Errorf("data block not found")
}
return nil, fmt.Errorf("data block not terminated")
}
func reshapeFloat64(values []float64, rows, cols int) ([][]float64, error) {
if rows*cols != len(values) {
return nil, fmt.Errorf("value count %d does not match %dx%d", len(values), rows, cols)
}
matrix := make([][]float64, rows)
for r := 0; r < rows; r++ {
start := r * cols
row := make([]float64, cols)
copy(row, values[start:start+cols])
matrix[r] = row
}
return matrix, nil
}
func transposeFloat64(matrix [][]float64) [][]float64 {
if len(matrix) == 0 || len(matrix[0]) == 0 {
return matrix
}
rows := len(matrix)
cols := len(matrix[0])
transposed := make([][]float64, cols)
for c := 0; c < cols; c++ {
transposed[c] = make([]float64, rows)
for r := 0; r < rows; r++ {
transposed[c][r] = matrix[r][c]
}
}
return transposed
}
func runH5Dump(filePath string, args ...string) ([]byte, error) {
cmdArgs := append(args, filePath)
cmd := exec.Command("h5dump", cmdArgs...)
out, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("h5dump %v failed: %w: %s", cmdArgs, err, strings.TrimSpace(string(out)))
}
return out, nil
}
func absFloat64(value float64) float64 {
if value < 0 {
return -value
}
return value
}
+32 -25
View File
@@ -3,10 +3,10 @@ package control_case
import (
"context"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"time"
"github.com/BurntSushi/toml"
"github.com/otiai10/copy"
@@ -22,16 +22,19 @@ type ControlCase struct {
}
type Params struct {
gorm.Model
ID uint `toml:"-" csv:"id"`
Rel float64 `toml:"rel" csv:"rel"`
RelC float64 `toml:"rel_c" csv:"rel_c"`
Le float64 `toml:"le" csv:"le"`
Pr float64 `toml:"pr" csv:"pr"`
Pe float64 `toml:"pe" csv:"pe"`
InitialCondition string `toml:"-" csv:"initial_condition"`
Time float64 `toml:"time" csv:"time"`
FolderPath string `toml:"-" csv:"-"`
ID uint `gorm:"primaryKey" json:"id" toml:"-" csv:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"`
Rel float64 `json:"rel" toml:"rel" csv:"rel"`
RelC float64 `json:"rel_c" toml:"rel_c" csv:"rel_c"`
Le float64 `json:"le" toml:"le" csv:"le"`
Pr float64 `json:"pr" toml:"pr" csv:"pr"`
Pe float64 `json:"pe" toml:"pe" csv:"pe"`
Ma float64 `json:"ma" toml:"ma" csv:"ma"`
InitialCondition string `json:"initial_condition" toml:"-" csv:"initial_condition"`
Time float64 `json:"time" toml:"time" csv:"time"`
FolderPath string `json:"folder_path" toml:"-" csv:"-"`
}
func (p *Params) ToTOML(filename string) error {
@@ -45,37 +48,40 @@ func (p *Params) ToTOML(filename string) error {
return encoder.Encode(p)
}
func (u *Params) AfterCreate(tx *gorm.DB) (err error) {
control_case := ControlCase{ParamsID: u.ID, Status: "N"}
tx.Create(&control_case)
return
}
func (p *Params) Exist() (bool, error) {
folderPath := filepath.Join("./", fmt.Sprintf("%d", p.ID))
return exists(folderPath)
}
func (p *Params) CreateFolder() {
func (p *Params) CreateFolder() error {
BIN_PATH := "./bio-convect"
folderPath := filepath.Join("./", fmt.Sprintf("%d", p.ID))
os.MkdirAll(folderPath, os.ModePerm)
if err := os.MkdirAll(folderPath, os.ModePerm); err != nil {
return err
}
fmt.Printf("folder: %s\n", folderPath)
execPath := filepath.Join("./", folderPath, BIN_PATH)
err := copy.Copy(BIN_PATH, execPath)
if err != nil {
log.Fatal(err)
return err
}
if p.InitialCondition != "E" {
startPath := filepath.Join("./", folderPath, "storag.h5")
if err := os.Chmod(execPath, 0o755); err != nil {
return err
}
if p.InitialCondition != "" && p.InitialCondition != "E" {
startPath := filepath.Join("./", folderPath, "storage.h5")
err = copy.Copy(p.InitialCondition, startPath)
if err != nil {
log.Fatal(err)
return err
}
}
p.ToTOML(filepath.Join(folderPath, "config.toml"))
if err := p.ToTOML(filepath.Join(folderPath, "config.toml")); err != nil {
return err
}
p.FolderPath = folderPath
return nil
}
func (p *Params) Run(ctx context.Context) {
func (p *Params) Run(ctx context.Context) error {
BIN_PATH := "./bio-convect"
cmd := exec.CommandContext(ctx, BIN_PATH)
cmd.Dir = p.FolderPath
@@ -86,7 +92,8 @@ func (p *Params) Run(ctx context.Context) {
err := cmd.Run()
fmt.Printf("afrer run")
if err != nil {
log.Fatal(err)
return err
}
return nil
}
+4
View File
@@ -14,6 +14,10 @@ func RegisterApp(r *gin.Engine, db *gorm.DB) {
params := NewParamsController(db)
go4rest.RegisterCRUDRoutes(r, "control_case", controller)
go4rest.RegisterCRUDRoutes(r, "params", params)
r.POST("/api/control_case/launch", controller.Launch)
r.POST("/api/control_case/:id/recalculate-analysis", controller.RecalculateAnalysis)
r.GET("/api/control_case/:id/chart-data", controller.ChartData)
r.GET("/api/control_case/:id/field-map", controller.FieldMap)
numJobs := 1
//jobs := make(chan Params, numJobs)
results := make(chan int, numJobs)
+41 -17
View File
@@ -5,11 +5,12 @@ import (
"errors"
"fmt"
"io/fs"
"log"
"os"
"path/filepath"
"sync"
"time"
"control/analize"
"github.com/gocarina/gocsv"
"gorm.io/gorm"
)
@@ -25,11 +26,40 @@ func exists(path string) (bool, error) {
return false, err
}
func worker(id int, jobs <-chan Params, ctx context.Context) {
func worker(id int, jobs <-chan ControlCase, ctx context.Context, db *gorm.DB) {
for j := range jobs {
fmt.Printf("Worker %d started job %d \n", id, j.ID)
j.CreateFolder()
j.Run(ctx)
p := j.Params
p.FolderPath = filepath.Join(".", fmt.Sprintf("%d", p.ID))
exists, err := p.Exist()
if err != nil {
fmt.Printf("Worker %d failed to check folder for job %d: %v\n", id, j.ID, err)
continue
}
if !exists {
if err := p.CreateFolder(); err != nil {
fmt.Printf("Worker %d failed to prepare job %d: %v\n", id, j.ID, err)
continue
}
if err := p.Run(ctx); err != nil {
fmt.Printf("Worker %d failed to run job %d: %v\n", id, j.ID, err)
continue
}
}
csvPath := filepath.Join(p.FolderPath, "foo.csv")
if _, err := analize.AnalyzeCSV(db, j.ID, j.Name, csvPath); err != nil {
fmt.Printf("Worker %d failed to analyze job %d: %v\n", id, j.ID, err)
continue
}
j.Status = "R"
if err := db.Model(&ControlCase{}).Where("id = ?", j.ID).Update("status", j.Status).Error; err != nil {
fmt.Printf("Worker %d failed to update status for job %d: %v\n", id, j.ID, err)
continue
}
time.Sleep(time.Second) // Имитация длительной задачи
fmt.Printf("Worker %d finished job %d \n", id, j.ID)
// results <- j * 2
@@ -39,13 +69,15 @@ func worker(id int, jobs <-chan Params, ctx context.Context) {
func readCSVWithGocsv() []Params {
file, err := os.Open("db.csv")
if err != nil {
log.Fatal(err)
fmt.Printf("failed to open csv: %v\n", err)
return nil
}
defer file.Close()
var params []Params
if err := gocsv.UnmarshalFile(file, &params); err != nil {
log.Fatal(err)
fmt.Printf("failed to parse csv: %v\n", err)
return nil
}
for _, person := range params {
@@ -73,7 +105,7 @@ func RunControllWorker(db *gorm.DB, results chan int) {
cancel()
}() // Cancel if main exits early
jobs := make(chan Params, numJobs)
jobs := make(chan ControlCase, numJobs)
//results := make(chan int, numJobs)
// Запуск воркеров
@@ -82,7 +114,7 @@ func RunControllWorker(db *gorm.DB, results chan int) {
wg.Add(1)
go func(w int) {
defer wg.Done()
worker(w, jobs, ctx)
worker(w, jobs, ctx, db)
}(w)
}
@@ -91,15 +123,7 @@ func RunControllWorker(db *gorm.DB, results chan int) {
cases := readFromDb(db)
// Отправка задач
for _, c := range cases {
p := c.Params
e, _ := p.Exist()
if e {
c.Status = "R"
db.Save((&c))
} else {
p.CreateFolder()
jobs <- p
}
jobs <- c
}
time.Sleep(10 * time.Second)
}