diff --git a/analize/controllers.go b/analize/controllers.go
new file mode 100644
index 0000000..3f5080a
--- /dev/null
+++ b/analize/controllers.go
@@ -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)
+}
diff --git a/analize/models.go b/analize/models.go
new file mode 100644
index 0000000..0bdad2a
--- /dev/null
+++ b/analize/models.go
@@ -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"`
+}
diff --git a/analize/routers.go b/analize/routers.go
new file mode 100644
index 0000000..e251c3f
--- /dev/null
+++ b/analize/routers.go
@@ -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)
+}
diff --git a/analize/service.go b/analize/service.go
new file mode 100644
index 0000000..7234310
--- /dev/null
+++ b/analize/service.go
@@ -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)
+}
diff --git a/control-ui/package-lock.json b/control-ui/package-lock.json
index 6a9fab9..35c1619 100644
--- a/control-ui/package-lock.json
+++ b/control-ui/package-lock.json
@@ -9,6 +9,8 @@
"version": "0.0.0",
"dependencies": {
"axios": "^1.18.1",
+ "bootstrap": "^5.3.8",
+ "echarts": "^6.1.0",
"pinia": "^3.0.4",
"vue": "beta",
"vue-router": "^5.1.0"
@@ -594,6 +596,17 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@popperjs/core": {
+ "version": "2.11.8",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
@@ -1383,6 +1396,25 @@
"url": "https://github.com/sponsors/antfu"
}
},
+ "node_modules/bootstrap": {
+ "version": "5.3.8",
+ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz",
+ "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/twbs"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/bootstrap"
+ }
+ ],
+ "license": "MIT",
+ "peerDependencies": {
+ "@popperjs/core": "^2.11.8"
+ }
+ },
"node_modules/browserslist": {
"version": "4.28.5",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz",
@@ -1659,6 +1691,22 @@
"node": ">= 0.4"
}
},
+ "node_modules/echarts": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.1.0.tgz",
+ "integrity": "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "2.3.0",
+ "zrender": "6.1.0"
+ }
+ },
+ "node_modules/echarts/node_modules/tslib": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
+ "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
+ "license": "0BSD"
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.389",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz",
@@ -3571,6 +3619,21 @@
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
+ },
+ "node_modules/zrender": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.1.0.tgz",
+ "integrity": "sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tslib": "2.3.0"
+ }
+ },
+ "node_modules/zrender/node_modules/tslib": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
+ "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
+ "license": "0BSD"
}
}
}
diff --git a/control-ui/package.json b/control-ui/package.json
index cd62939..3291a47 100644
--- a/control-ui/package.json
+++ b/control-ui/package.json
@@ -12,6 +12,8 @@
},
"dependencies": {
"axios": "^1.18.1",
+ "bootstrap": "^5.3.8",
+ "echarts": "^6.1.0",
"pinia": "^3.0.4",
"vue": "beta",
"vue-router": "^5.1.0"
diff --git a/control-ui/src/App.vue b/control-ui/src/App.vue
index 73b8494..7ade7f7 100644
--- a/control-ui/src/App.vue
+++ b/control-ui/src/App.vue
@@ -1,19 +1,33 @@
- You did it!
-
- Visit {{ res }}
- vuejs.org to
- read the documentation
-
-
+
+
-
+
+
+
+
+
diff --git a/control-ui/src/api.ts b/control-ui/src/api.ts
index 43c9ff3..1fc9f82 100644
--- a/control-ui/src/api.ts
+++ b/control-ui/src/api.ts
@@ -1,5 +1,89 @@
-import { createModelApi } from "@/api_client.ts";
-import type { Params, ControlCase } from "@/models.ts";
+import { createModelApi, apiClient } from "@/api_client.ts";
+import type { Params, ControlCase, Analize } from "@/models.ts";
export const paramsApi = createModelApi("params");
-export const controlCaseApi = createModelApi("control-case");
+
+export const controlCaseApi = createModelApi("control_case");
+export const analizeApi = createModelApi("analize");
+
+export interface CsvChartResponse {
+ columns: string[];
+ rows: Array>;
+}
+
+export interface FieldMapResponse {
+ requested_t: number;
+ stage_t: number;
+ rows: number;
+ cols: number;
+ fields: Record;
+}
+
+export async function getControlCaseChartData(id: number) {
+ const response = await apiClient.get(
+ `/control_case/${id}/chart-data`,
+ );
+ return response.data;
+}
+
+export async function getControlCaseFieldMap(id: number, time: number) {
+ const response = await apiClient.get(
+ `/control_case/${id}/field-map`,
+ {
+ params: { time },
+ },
+ );
+ return response.data;
+}
+
+export interface LaunchControlCasePayload {
+ params_id: number;
+ name?: string;
+}
+
+export async function launchControlCase(payload: LaunchControlCasePayload) {
+ const response = await apiClient.post(
+ "/control_case/launch",
+ payload,
+ );
+ return response.data;
+}
+
+export async function recalculateControlCaseAnalysis(id: number) {
+ const response = await apiClient.post(
+ `/control_case/${id}/recalculate-analysis`,
+ );
+ return response.data;
+}
+
+export async function recalculateAnalize(id: number) {
+ const response = await apiClient.post(`/analize/${id}/recalculate`);
+ return response.data;
+}
+
+export interface AnalizeSeriesPoint {
+ case_id: number;
+ case_name: string;
+ x: number;
+ omega: number;
+ psi_max: number;
+}
+
+export interface AnalizeSeriesResponse {
+ parameter: string;
+ label: string;
+ points: AnalizeSeriesPoint[];
+}
+
+export interface AnalizeSeriesFilters {
+ filter_parameter?: string;
+ filter_min?: number;
+ filter_max?: number;
+}
+
+export async function getAnalizeSeries(parameter: string, filters?: AnalizeSeriesFilters) {
+ const response = await apiClient.get("/analize/series", {
+ params: { parameter, ...(filters ?? {}) },
+ });
+ return response.data;
+}
diff --git a/control-ui/src/api_client.ts b/control-ui/src/api_client.ts
index 48407e7..9740483 100644
--- a/control-ui/src/api_client.ts
+++ b/control-ui/src/api_client.ts
@@ -1,12 +1,12 @@
import axios from "axios";
export interface BaseEntity {
- id: number;
+ id?: number;
+ ID?: number;
}
export interface ListParams {
page?: number;
- page_size?: number;
search?: string;
ordering?: string;
[key: string]: string | number | boolean | undefined;
@@ -121,7 +121,6 @@ export function createModelApi<
params: {
...(params ?? {}),
page: 1,
- page_size: params?.page_size ?? 100,
},
},
);
diff --git a/control-ui/src/main.ts b/control-ui/src/main.ts
index fda1e6e..2097264 100644
--- a/control-ui/src/main.ts
+++ b/control-ui/src/main.ts
@@ -1,5 +1,6 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
+import 'bootstrap/dist/css/bootstrap.min.css'
import App from './App.vue'
import router from './router'
diff --git a/control-ui/src/models.ts b/control-ui/src/models.ts
index b9edb40..aa5f560 100644
--- a/control-ui/src/models.ts
+++ b/control-ui/src/models.ts
@@ -1,23 +1,44 @@
+export interface GormDeletedAt {
+ Time: string;
+ Valid: boolean;
+}
+
export interface Params {
id: number;
+ created_at: string;
+ updated_at: string;
+ deleted_at: GormDeletedAt | null;
rel: number;
- relC: number;
+ rel_c: number;
le: number;
pr: number;
pe: number;
- initialCondition: string;
+ ma: number;
+ initial_condition: string;
time: number;
- folderPath: string;
+ folder_path: string;
}
export interface ControlCase {
- id: number;
- createdAt: string;
- updatedAt: string;
- deletedAt: string | null;
-
- name: string;
- paramsId: number;
- params: Params;
- status: string;
+ ID: number;
+ CreatedAt: string;
+ UpdatedAt: string;
+ DeletedAt: GormDeletedAt | null;
+ Name: string;
+ ParamsID: number;
+ Params: Params;
+ Status: string;
+}
+
+export interface Analize {
+ ID: number;
+ CreatedAt: string;
+ UpdatedAt: string;
+ DeletedAt: GormDeletedAt | null;
+ Name: string;
+ CaseID: number;
+ CaseName: string;
+ PsiMax: number;
+ PsiLMax: number;
+ Omega: number;
}
diff --git a/control-ui/src/router/index.ts b/control-ui/src/router/index.ts
index e1eab52..25f2866 100644
--- a/control-ui/src/router/index.ts
+++ b/control-ui/src/router/index.ts
@@ -2,7 +2,44 @@ import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
- routes: [],
+ routes: [
+ {
+ path: '/',
+ redirect: '/control-cases',
+ },
+ {
+ path: '/control-cases',
+ name: 'control-cases',
+ component: () => import('@/views/ControlCaseListView.vue'),
+ },
+ {
+ path: '/control-cases/:id',
+ name: 'control-case-detail',
+ component: () => import('@/views/ControlCaseDetailView.vue'),
+ props: true,
+ },
+ {
+ path: '/control-cases/:id/chart',
+ name: 'control-case-chart',
+ component: () => import('@/views/ControlCaseChartView.vue'),
+ props: true,
+ },
+ {
+ path: '/params',
+ name: 'params-list',
+ component: () => import('@/views/ParamsListView.vue'),
+ },
+ {
+ path: '/analize',
+ name: 'analize-list',
+ component: () => import('@/views/AnalizeListView.vue'),
+ },
+ {
+ path: '/analize/chart',
+ name: 'analize-chart',
+ component: () => import('@/views/AnalizeChartView.vue'),
+ },
+ ],
})
export default router
diff --git a/control-ui/src/views/AnalizeChartView.vue b/control-ui/src/views/AnalizeChartView.vue
new file mode 100644
index 0000000..26c779d
--- /dev/null
+++ b/control-ui/src/views/AnalizeChartView.vue
@@ -0,0 +1,538 @@
+
+
+
+
+
+
+
Analize graph
+
Omega и PsiMax по выбранному параметру
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ error }}
+
+
+
+
+
+
+
Загрузка...
+
+ {{ selectedCaseError }}
+
+
+
+
+
Название
+
{{ selectedCase.Name }}
+
+
+
Статус
+
{{ selectedCase.Status }}
+
+
+
Params ID
+
{{ selectedCase.ParamsID }}
+
+
+
+
+ ControlCase
+
+
+
+
+ | {{ key }} |
+ {{ formatValue(value) }} |
+
+
+
+
+
+
+
+
Params
+
+
+
+ | {{ key }} |
+ {{ formatValue(value) }} |
+
+
+
+
+
+
+
+
+
+ Нажмите на точку на верхнем графике, чтобы увидеть расчетный случай.
+
+
+
+
+
+
+
+
+
+
+
Загрузка карты полей...
+
+
+ {{ selectedFieldMapError }}
+
+
+ Нажмите на точку графика `psi_m`, чтобы показать карту полей.
+
+
+
+
+
+
diff --git a/control-ui/src/views/AnalizeListView.vue b/control-ui/src/views/AnalizeListView.vue
new file mode 100644
index 0000000..adc615a
--- /dev/null
+++ b/control-ui/src/views/AnalizeListView.vue
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
Analize
+
Результаты обработки расчетных случаев
+
+
+
График
+
+
+ {{ error }}
+
+
+
+
Загрузка...
+
+
+
+
+ | ID |
+ Название |
+ Case |
+ PsiMax |
+ PsiLMax |
+ Omega |
+ Создан |
+ Действия |
+
+
+
+
+ | Записи не найдены |
+
+
+ | {{ item.ID }} |
+ {{ item.Name || '—' }} |
+
+
+
+ {{ item.CaseName || '—' }}
+
+ #{{ item.CaseID }}
+
+ |
+ {{ item.PsiMax }} |
+ {{ item.PsiLMax }} |
+ {{ item.Omega }} |
+ {{ formatDate(item.CreatedAt) }} |
+
+
+ |
+
+
+
+
+
+
+
+
diff --git a/control-ui/src/views/ControlCaseChartView.vue b/control-ui/src/views/ControlCaseChartView.vue
new file mode 100644
index 0000000..aa1a5ca
--- /dev/null
+++ b/control-ui/src/views/ControlCaseChartView.vue
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+ ← Назад к кейсу
+
+
График control case {{ id }}
+
+
+
+
+ {{ error }}
+
+
+
+
diff --git a/control-ui/src/views/ControlCaseDetailView.vue b/control-ui/src/views/ControlCaseDetailView.vue
new file mode 100644
index 0000000..e577c0d
--- /dev/null
+++ b/control-ui/src/views/ControlCaseDetailView.vue
@@ -0,0 +1,162 @@
+
+
+
+
+
+
+
+ ← Назад к списку
+
+
Control case {{ id }}
+
+
+ График
+
+
+
+
+ {{ error }}
+ {{ statusMessage }}
+
+
+
+
+
+
+
+
+ - ID
+ - {{ item.ID }}
+
+ - Params ID
+ - {{ item.ParamsID }}
+
+ - Создан
+ - {{ formatDate(item.CreatedAt) }}
+
+ - Обновлён
+ - {{ formatDate(item.UpdatedAt) }}
+
+ - Удалён
+ - {{ formatValue(item.DeletedAt) }}
+
+
+
+
+
+
+
+
+
+
+
+ | {{ key }} |
+ {{ formatValue(value) }} |
+
+
+
+
+
+
+
+
+ Запись не найдена
+
+
diff --git a/control-ui/src/views/ControlCaseListView.vue b/control-ui/src/views/ControlCaseListView.vue
new file mode 100644
index 0000000..8ec0c84
--- /dev/null
+++ b/control-ui/src/views/ControlCaseListView.vue
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
Control cases
+
Всего записей: {{ total }}
+
+
+
+
+ {{ error }}
+
+
+
+
Загрузка...
+
+
+
+
+ | ID |
+ Название |
+ Статус |
+ Params ID |
+ Создан |
+ Действия |
+
+
+
+
+ | Записи не найдены |
+
+
+ | {{ item.ID }} |
+ {{ item.Name || '—' }} |
+ {{ item.Status || '—' }} |
+ {{ item.ParamsID }} |
+ {{ formatDate(item.CreatedAt) }} |
+
+
+ Открыть
+
+ |
+
+
+
+
+
+
+ Показано {{ pageStart }}-{{ pageEnd }} из {{ totalCount }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/control-ui/src/views/ParamsListView.vue b/control-ui/src/views/ParamsListView.vue
new file mode 100644
index 0000000..080ae7d
--- /dev/null
+++ b/control-ui/src/views/ParamsListView.vue
@@ -0,0 +1,646 @@
+
+
+
+
+
+
+
Params
+
Таблица параметров с редактированием
+
+
+
+
+
+ {{ error }}
+
+
+
+ {{ batchMessage }}
+
+
+
+
+
+
+
Загрузка...
+
+
+
+ Показано {{ pageStart }}-{{ pageEnd }} из {{ totalCount }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/control_case/controllers.go b/control_case/controllers.go
index 3576c4b..37fa1eb 100644
--- a/control_case/controllers.go
+++ b/control_case/controllers.go
@@ -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(¶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").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)
+}
diff --git a/control_case/hdf5.go b/control_case/hdf5.go
new file mode 100644
index 0000000..502516b
--- /dev/null
+++ b/control_case/hdf5.go
@@ -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
+}
diff --git a/control_case/models.go b/control_case/models.go
index ef2808a..8848cd1 100644
--- a/control_case/models.go
+++ b/control_case/models.go
@@ -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
}
diff --git a/control_case/routers.go b/control_case/routers.go
index d81120a..332f53e 100644
--- a/control_case/routers.go
+++ b/control_case/routers.go
@@ -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)
diff --git a/control_case/services.go b/control_case/services.go
index e1e4530..004d24f 100644
--- a/control_case/services.go
+++ b/control_case/services.go
@@ -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, ¶ms); 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)
}
diff --git a/main.go b/main.go
index 9e7ec9c..c9fb1f1 100644
--- a/main.go
+++ b/main.go
@@ -3,7 +3,9 @@ package main
import (
"log"
+ "control/analize"
"control/control_case"
+
"github.com/gin-gonic/gin"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
@@ -19,6 +21,7 @@ func main() {
_ = db
control_case.RegisterApp(r, db)
+ analize.RegisterApp(r, db)
// numJobs := 10
//jobs := make(chan control_case.Params, numJobs)
//results := make(chan int, numJobs)