diff --git a/analize/models.go b/analize/models.go index 0bdad2a..8ea015e 100644 --- a/analize/models.go +++ b/analize/models.go @@ -1,13 +1,20 @@ package analize -import "gorm.io/gorm" +import ( + "time" + + "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"` + ID uint `gorm:"primaryKey" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"` + 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/service.go b/analize/service.go index caf488a..d4a526d 100644 --- a/analize/service.go +++ b/analize/service.go @@ -1,17 +1,12 @@ package analize import ( - "encoding/csv" "errors" "fmt" - "math" - "os" - "strconv" - "strings" "time" "control/control_case" - "gonum.org/v1/gonum/dsp/fourier" + "control/series" "gorm.io/gorm" ) @@ -40,81 +35,27 @@ func RunAnalyzeWorker(db *gorm.DB) { } func AnalyzeCSV(db *gorm.DB, caseID uint, caseName, csvPath string) (*Analize, error) { - file, err := os.Open(csvPath) + data, err := series.ReadCSV(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 { + if len(data.Rows) == 0 { 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") + columns, err := data.NumericColumns("t", "psi_m", "psi_l") 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) - } + times, psiM, psiL := columns[0], columns[1], columns[2] if len(psiL) == 0 { return nil, fmt.Errorf("no numeric rows found in csv") } - psiMax := maxFloat(psiM) - psiLMax := maxFloat(psiL) - omega := mainAngularFrequency(times, psiL) + psiMax := series.MaxLastHalf(psiM) + psiLMax := series.MaxLastHalf(psiL) + omega := series.MainAngularFrequency(times, psiL) result := &Analize{ Name: fmt.Sprintf("analysis-%d", caseID), @@ -143,94 +84,3 @@ func AnalyzeCSV(db *gorm.DB, caseID uint, caseName, csvPath string) (*Analize, e } 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[len(values)-1] - for _, v := range values[len(values):] { - 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/src/api.ts b/control-ui/src/api.ts index c7fb7d1..e95933e 100644 --- a/control-ui/src/api.ts +++ b/control-ui/src/api.ts @@ -3,8 +3,6 @@ import type { Params, ControlCase, Analize, InitialCondition } from "@/models.ts export const paramsApi = createModelApi("params"); -export const controlCaseApi = createModelApi("control_case"); -export const analizeApi = createModelApi("analize"); export const initialConditionApi = createModelApi("initial_condition"); export interface CsvChartResponse { @@ -37,61 +35,53 @@ export interface InitialConditionResponse { initial_condition: InitialCondition; } -export async function getControlCaseChartData(id: number) { - const response = await apiClient.get( - `/control_case/${id}/chart-data`, - ); - return response.data; -} - -export async function getControlCasePsiSpectrum(id: number) { - const response = await apiClient.get( - `/control_case/${id}/psi-spectrum`, - ); - 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 async function createControlCaseInitialCondition(id: number) { - const response = await apiClient.post( - `/control_case/${id}/initial-condition`, - ); - 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 const controlCaseApi = { + ...createModelApi("control_case"), -export async function recalculateControlCaseAnalysis(id: number) { - const response = await apiClient.post( - `/control_case/${id}/recalculate-analysis`, - ); - return response.data; -} + async chartData(id: number) { + const response = await apiClient.get( + `/control_case/${id}/chart-data`, + ); + return response.data; + }, -export async function recalculateAnalize(id: number) { - const response = await apiClient.post(`/analize/${id}/recalculate`); - return response.data; -} + async psiSpectrum(id: number) { + const response = await apiClient.get( + `/control_case/${id}/psi-spectrum`, + ); + return response.data; + }, + + async fieldMap(id: number, time: number) { + const response = await apiClient.get( + `/control_case/${id}/field-map`, + { + params: { time }, + }, + ); + return response.data; + }, + + async createInitialCondition(id: number) { + const response = await apiClient.post( + `/control_case/${id}/initial-condition`, + ); + return response.data; + }, + + async launch(payload: LaunchControlCasePayload) { + const response = await apiClient.post( + "/control_case/launch", + payload, + ); + return response.data; + }, +}; export interface AnalizeSeriesPoint { case_id: number; @@ -123,9 +113,25 @@ export interface AnalizeSeriesFilters { group_parameter?: string; } -export async function getAnalizeSeries(parameter: string, filters?: AnalizeSeriesFilters) { - const response = await apiClient.get("/analize/series", { - params: { parameter, ...(filters ?? {}) }, - }); - return response.data; -} +export const analizeApi = { + ...createModelApi("analize"), + + async recalculate(id: number) { + const response = await apiClient.post(`/analize/${id}/recalculate`); + return response.data; + }, + + async recalculateControlCase(id: number) { + const response = await apiClient.post( + `/control_case/${id}/recalculate-analysis`, + ); + return response.data; + }, + + async series(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 524591e..599261e 100644 --- a/control-ui/src/api_client.ts +++ b/control-ui/src/api_client.ts @@ -2,7 +2,6 @@ import axios from "axios"; export interface BaseEntity { id?: number; - ID?: number; } export interface ListParams { diff --git a/control-ui/src/components/params/ParamsEditCard.vue b/control-ui/src/components/params/ParamsEditCard.vue index e841ebe..bcc7cc2 100644 --- a/control-ui/src/components/params/ParamsEditCard.vue +++ b/control-ui/src/components/params/ParamsEditCard.vue @@ -53,8 +53,8 @@ defineEmits<{ diff --git a/control-ui/src/components/params/ParamsQuickBatchCard.vue b/control-ui/src/components/params/ParamsQuickBatchCard.vue index 3899479..e69e9b5 100644 --- a/control-ui/src/components/params/ParamsQuickBatchCard.vue +++ b/control-ui/src/components/params/ParamsQuickBatchCard.vue @@ -97,8 +97,8 @@ defineEmits<{ diff --git a/control-ui/src/components/params/ParamsTableCard.vue b/control-ui/src/components/params/ParamsTableCard.vue index 5b3a451..7de5922 100644 --- a/control-ui/src/components/params/ParamsTableCard.vue +++ b/control-ui/src/components/params/ParamsTableCard.vue @@ -80,8 +80,8 @@ defineEmits<{ diff --git a/control-ui/src/models.ts b/control-ui/src/models.ts index c3ca5b6..181c2c9 100644 --- a/control-ui/src/models.ts +++ b/control-ui/src/models.ts @@ -21,34 +21,34 @@ export interface Params { } export interface InitialCondition { - ID: number; - CreatedAt: string; - UpdatedAt: string; - DeletedAt: GormDeletedAt | null; + id: number; + created_at: string; + updated_at: string; + deleted_at: GormDeletedAt | null; name: string; file_path: string; } export interface ControlCase { - ID: number; - CreatedAt: string; - UpdatedAt: string; - DeletedAt: GormDeletedAt | null; + id: number; + created_at: string; + updated_at: string; + deleted_at: GormDeletedAt | null; name: string; - paramsID: number; + params_id: 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; + id: number; + created_at: string; + updated_at: string; + deleted_at: GormDeletedAt | null; + name: string; + case_id: number; + case_name: string; + psi_max: number; + psi_l_max: number; + omega: number; } diff --git a/control-ui/src/views/AnalizeChartView.vue b/control-ui/src/views/AnalizeChartView.vue index 2d62b0b..154b7d5 100644 --- a/control-ui/src/views/AnalizeChartView.vue +++ b/control-ui/src/views/AnalizeChartView.vue @@ -1,7 +1,7 @@