fix
This commit is contained in:
+9
-2
@@ -1,9 +1,16 @@
|
|||||||
package analize
|
package analize
|
||||||
|
|
||||||
import "gorm.io/gorm"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
type Analize struct {
|
type Analize struct {
|
||||||
gorm.Model
|
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"`
|
Name string `json:"name"`
|
||||||
CaseID uint `json:"case_id"`
|
CaseID uint `json:"case_id"`
|
||||||
CaseName string `json:"case_name"`
|
CaseName string `json:"case_name"`
|
||||||
|
|||||||
+8
-158
@@ -1,17 +1,12 @@
|
|||||||
package analize
|
package analize
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/csv"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"control/control_case"
|
"control/control_case"
|
||||||
"gonum.org/v1/gonum/dsp/fourier"
|
"control/series"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,81 +35,27 @@ func RunAnalyzeWorker(db *gorm.DB) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func AnalyzeCSV(db *gorm.DB, caseID uint, caseName, csvPath string) (*Analize, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
if len(data.Rows) == 0 {
|
||||||
|
|
||||||
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")
|
return nil, fmt.Errorf("csv has no data rows")
|
||||||
}
|
}
|
||||||
|
|
||||||
headers := make(map[string]int, len(records[0]))
|
columns, err := data.NumericColumns("t", "psi_m", "psi_l")
|
||||||
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
psiMIndex, err := getIndex("psi_m")
|
times, psiM, psiL := columns[0], columns[1], columns[2]
|
||||||
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 {
|
if len(psiL) == 0 {
|
||||||
return nil, fmt.Errorf("no numeric rows found in csv")
|
return nil, fmt.Errorf("no numeric rows found in csv")
|
||||||
}
|
}
|
||||||
|
|
||||||
psiMax := maxFloat(psiM)
|
psiMax := series.MaxLastHalf(psiM)
|
||||||
psiLMax := maxFloat(psiL)
|
psiLMax := series.MaxLastHalf(psiL)
|
||||||
omega := mainAngularFrequency(times, psiL)
|
omega := series.MainAngularFrequency(times, psiL)
|
||||||
|
|
||||||
result := &Analize{
|
result := &Analize{
|
||||||
Name: fmt.Sprintf("analysis-%d", caseID),
|
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
|
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)
|
|
||||||
}
|
|
||||||
|
|||||||
+37
-31
@@ -3,8 +3,6 @@ import type { Params, ControlCase, Analize, InitialCondition } from "@/models.ts
|
|||||||
|
|
||||||
export const paramsApi = createModelApi<Params>("params");
|
export const paramsApi = createModelApi<Params>("params");
|
||||||
|
|
||||||
export const controlCaseApi = createModelApi<ControlCase>("control_case");
|
|
||||||
export const analizeApi = createModelApi<Analize>("analize");
|
|
||||||
export const initialConditionApi = createModelApi<InitialCondition>("initial_condition");
|
export const initialConditionApi = createModelApi<InitialCondition>("initial_condition");
|
||||||
|
|
||||||
export interface CsvChartResponse {
|
export interface CsvChartResponse {
|
||||||
@@ -37,21 +35,29 @@ export interface InitialConditionResponse {
|
|||||||
initial_condition: InitialCondition;
|
initial_condition: InitialCondition;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getControlCaseChartData(id: number) {
|
export interface LaunchControlCasePayload {
|
||||||
|
params_id: number;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const controlCaseApi = {
|
||||||
|
...createModelApi<ControlCase>("control_case"),
|
||||||
|
|
||||||
|
async chartData(id: number) {
|
||||||
const response = await apiClient.get<CsvChartResponse>(
|
const response = await apiClient.get<CsvChartResponse>(
|
||||||
`/control_case/${id}/chart-data`,
|
`/control_case/${id}/chart-data`,
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
},
|
||||||
|
|
||||||
export async function getControlCasePsiSpectrum(id: number) {
|
async psiSpectrum(id: number) {
|
||||||
const response = await apiClient.get<PsiSpectrumResponse>(
|
const response = await apiClient.get<PsiSpectrumResponse>(
|
||||||
`/control_case/${id}/psi-spectrum`,
|
`/control_case/${id}/psi-spectrum`,
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
},
|
||||||
|
|
||||||
export async function getControlCaseFieldMap(id: number, time: number) {
|
async fieldMap(id: number, time: number) {
|
||||||
const response = await apiClient.get<FieldMapResponse>(
|
const response = await apiClient.get<FieldMapResponse>(
|
||||||
`/control_case/${id}/field-map`,
|
`/control_case/${id}/field-map`,
|
||||||
{
|
{
|
||||||
@@ -59,39 +65,23 @@ export async function getControlCaseFieldMap(id: number, time: number) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
},
|
||||||
|
|
||||||
export async function createControlCaseInitialCondition(id: number) {
|
async createInitialCondition(id: number) {
|
||||||
const response = await apiClient.post<InitialConditionResponse>(
|
const response = await apiClient.post<InitialConditionResponse>(
|
||||||
`/control_case/${id}/initial-condition`,
|
`/control_case/${id}/initial-condition`,
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
},
|
||||||
|
|
||||||
export interface LaunchControlCasePayload {
|
async launch(payload: LaunchControlCasePayload) {
|
||||||
params_id: number;
|
|
||||||
name?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function launchControlCase(payload: LaunchControlCasePayload) {
|
|
||||||
const response = await apiClient.post<ControlCase>(
|
const response = await apiClient.post<ControlCase>(
|
||||||
"/control_case/launch",
|
"/control_case/launch",
|
||||||
payload,
|
payload,
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
},
|
||||||
|
};
|
||||||
export async function recalculateControlCaseAnalysis(id: number) {
|
|
||||||
const response = await apiClient.post<Analize>(
|
|
||||||
`/control_case/${id}/recalculate-analysis`,
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function recalculateAnalize(id: number) {
|
|
||||||
const response = await apiClient.post<Analize>(`/analize/${id}/recalculate`);
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AnalizeSeriesPoint {
|
export interface AnalizeSeriesPoint {
|
||||||
case_id: number;
|
case_id: number;
|
||||||
@@ -123,9 +113,25 @@ export interface AnalizeSeriesFilters {
|
|||||||
group_parameter?: string;
|
group_parameter?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAnalizeSeries(parameter: string, filters?: AnalizeSeriesFilters) {
|
export const analizeApi = {
|
||||||
|
...createModelApi<Analize>("analize"),
|
||||||
|
|
||||||
|
async recalculate(id: number) {
|
||||||
|
const response = await apiClient.post<Analize>(`/analize/${id}/recalculate`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async recalculateControlCase(id: number) {
|
||||||
|
const response = await apiClient.post<Analize>(
|
||||||
|
`/control_case/${id}/recalculate-analysis`,
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async series(parameter: string, filters?: AnalizeSeriesFilters) {
|
||||||
const response = await apiClient.get<AnalizeSeriesResponse>("/analize/series", {
|
const response = await apiClient.get<AnalizeSeriesResponse>("/analize/series", {
|
||||||
params: { parameter, ...(filters ?? {}) },
|
params: { parameter, ...(filters ?? {}) },
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import axios from "axios";
|
|||||||
|
|
||||||
export interface BaseEntity {
|
export interface BaseEntity {
|
||||||
id?: number;
|
id?: number;
|
||||||
ID?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ListParams {
|
export interface ListParams {
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ defineEmits<{
|
|||||||
<label class="form-label">initial_condition</label>
|
<label class="form-label">initial_condition</label>
|
||||||
<select v-model="form.initial_condition_id" class="form-select">
|
<select v-model="form.initial_condition_id" class="form-select">
|
||||||
<option :value="null">Не задано</option>
|
<option :value="null">Не задано</option>
|
||||||
<option v-for="condition in initialConditions" :key="condition.ID" :value="condition.ID">
|
<option v-for="condition in initialConditions" :key="condition.id" :value="condition.id">
|
||||||
{{ condition.name || `#${condition.ID}` }} — {{ condition.file_path }}
|
{{ condition.name || `#${condition.id}` }} — {{ condition.file_path }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -97,8 +97,8 @@ defineEmits<{
|
|||||||
<td>
|
<td>
|
||||||
<select v-model="batchForm.fixed.initial_condition_id" class="form-select">
|
<select v-model="batchForm.fixed.initial_condition_id" class="form-select">
|
||||||
<option :value="null">Не задано</option>
|
<option :value="null">Не задано</option>
|
||||||
<option v-for="condition in initialConditions" :key="condition.ID" :value="condition.ID">
|
<option v-for="condition in initialConditions" :key="condition.id" :value="condition.id">
|
||||||
{{ condition.name || `#${condition.ID}` }} — {{ condition.file_path }}
|
{{ condition.name || `#${condition.id}` }} — {{ condition.file_path }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -80,8 +80,8 @@ defineEmits<{
|
|||||||
<td>
|
<td>
|
||||||
<select v-model="createForm.initial_condition_id" class="form-select form-select-sm">
|
<select v-model="createForm.initial_condition_id" class="form-select form-select-sm">
|
||||||
<option :value="null">—</option>
|
<option :value="null">—</option>
|
||||||
<option v-for="condition in initialConditions" :key="condition.ID" :value="condition.ID">
|
<option v-for="condition in initialConditions" :key="condition.id" :value="condition.id">
|
||||||
{{ condition.name || `#${condition.ID}` }}
|
{{ condition.name || `#${condition.id}` }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
+19
-19
@@ -21,34 +21,34 @@ export interface Params {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface InitialCondition {
|
export interface InitialCondition {
|
||||||
ID: number;
|
id: number;
|
||||||
CreatedAt: string;
|
created_at: string;
|
||||||
UpdatedAt: string;
|
updated_at: string;
|
||||||
DeletedAt: GormDeletedAt | null;
|
deleted_at: GormDeletedAt | null;
|
||||||
name: string;
|
name: string;
|
||||||
file_path: string;
|
file_path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ControlCase {
|
export interface ControlCase {
|
||||||
ID: number;
|
id: number;
|
||||||
CreatedAt: string;
|
created_at: string;
|
||||||
UpdatedAt: string;
|
updated_at: string;
|
||||||
DeletedAt: GormDeletedAt | null;
|
deleted_at: GormDeletedAt | null;
|
||||||
name: string;
|
name: string;
|
||||||
paramsID: number;
|
params_id: number;
|
||||||
params: Params;
|
params: Params;
|
||||||
status: string;
|
status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Analize {
|
export interface Analize {
|
||||||
ID: number;
|
id: number;
|
||||||
CreatedAt: string;
|
created_at: string;
|
||||||
UpdatedAt: string;
|
updated_at: string;
|
||||||
DeletedAt: GormDeletedAt | null;
|
deleted_at: GormDeletedAt | null;
|
||||||
Name: string;
|
name: string;
|
||||||
CaseID: number;
|
case_id: number;
|
||||||
CaseName: string;
|
case_name: string;
|
||||||
PsiMax: number;
|
psi_max: number;
|
||||||
PsiLMax: number;
|
psi_l_max: number;
|
||||||
Omega: number;
|
omega: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
import { controlCaseApi, getAnalizeSeries, getControlCaseChartData, getControlCaseFieldMap, getControlCasePsiSpectrum } from '@/api.ts'
|
import { analizeApi, controlCaseApi } from '@/api.ts'
|
||||||
import { formatApiError } from '@/api_client.ts'
|
import { formatApiError } from '@/api_client.ts'
|
||||||
import type { ControlCase } from '@/models.ts'
|
import type { ControlCase } from '@/models.ts'
|
||||||
import { controlCaseStatusMeta } from '@/statuses.ts'
|
import { controlCaseStatusMeta } from '@/statuses.ts'
|
||||||
@@ -446,7 +446,7 @@ async function loadData() {
|
|||||||
error.value = ''
|
error.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await getAnalizeSeries(selectedParameter.value, {
|
const response = await analizeApi.series(selectedParameter.value, {
|
||||||
group_parameter: groupParameter.value || undefined,
|
group_parameter: groupParameter.value || undefined,
|
||||||
filter_parameter: filterParameter.value || undefined,
|
filter_parameter: filterParameter.value || undefined,
|
||||||
filter_min: filterMin.value === '' ? undefined : Number(filterMin.value),
|
filter_min: filterMin.value === '' ? undefined : Number(filterMin.value),
|
||||||
@@ -498,8 +498,8 @@ async function loadSelectedCase(caseId: number) {
|
|||||||
try {
|
try {
|
||||||
const [caseData, csvData, spectrumData] = await Promise.all([
|
const [caseData, csvData, spectrumData] = await Promise.all([
|
||||||
controlCaseApi.retrieve(caseId),
|
controlCaseApi.retrieve(caseId),
|
||||||
getControlCaseChartData(caseId),
|
controlCaseApi.chartData(caseId),
|
||||||
getControlCasePsiSpectrum(caseId),
|
controlCaseApi.psiSpectrum(caseId),
|
||||||
])
|
])
|
||||||
|
|
||||||
selectedCase.value = caseData
|
selectedCase.value = caseData
|
||||||
@@ -543,7 +543,7 @@ async function loadFieldMap(time: number) {
|
|||||||
selectedFieldMapError.value = ''
|
selectedFieldMapError.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
selectedFieldMap.value = await getControlCaseFieldMap(selectedCaseId.value, time)
|
selectedFieldMap.value = await controlCaseApi.fieldMap(selectedCaseId.value, time)
|
||||||
renderFieldMap()
|
renderFieldMap()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
selectedFieldMapError.value = formatApiError(err, 'Не удалось загрузить карту полей.')
|
selectedFieldMapError.value = formatApiError(err, 'Не удалось загрузить карту полей.')
|
||||||
@@ -711,7 +711,7 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-lg-4">
|
<div class="col-12 col-lg-4">
|
||||||
<div class="text-muted small">Params ID</div>
|
<div class="text-muted small">Params ID</div>
|
||||||
<div class="fw-semibold">{{ selectedCase.paramsID }}</div>
|
<div class="fw-semibold">{{ selectedCase.params_id }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { RouterLink } from 'vue-router'
|
import { RouterLink } from 'vue-router'
|
||||||
import { analizeApi, recalculateAnalize } from '@/api.ts'
|
import { analizeApi } from '@/api.ts'
|
||||||
import { formatApiError, isPaginatedResponse } from '@/api_client.ts'
|
import { formatApiError, isPaginatedResponse } from '@/api_client.ts'
|
||||||
import type { Analize } from '@/models.ts'
|
import type { Analize } from '@/models.ts'
|
||||||
|
|
||||||
@@ -30,16 +30,16 @@ const pageEnd = computed(() => {
|
|||||||
function normalizeAnalize(item: Analize | Record<string, unknown>): Analize {
|
function normalizeAnalize(item: Analize | Record<string, unknown>): Analize {
|
||||||
const record = item as Record<string, unknown>
|
const record = item as Record<string, unknown>
|
||||||
return {
|
return {
|
||||||
ID: Number(record.ID ?? record.id ?? 0),
|
id: Number(record.id ?? 0),
|
||||||
CreatedAt: String(record.CreatedAt ?? record.createdAt ?? ''),
|
created_at: String(record.created_at ?? ''),
|
||||||
UpdatedAt: String(record.UpdatedAt ?? record.updatedAt ?? ''),
|
updated_at: String(record.updated_at ?? ''),
|
||||||
DeletedAt: (record.DeletedAt ?? record.deletedAt ?? null) as Analize['DeletedAt'],
|
deleted_at: (record.deleted_at ?? null) as Analize['deleted_at'],
|
||||||
Name: String(record.Name ?? record.name ?? ''),
|
name: String(record.name ?? ''),
|
||||||
CaseID: Number(record.CaseID ?? record.case_id ?? 0),
|
case_id: Number(record.case_id ?? 0),
|
||||||
CaseName: String(record.CaseName ?? record.case_name ?? ''),
|
case_name: String(record.case_name ?? ''),
|
||||||
PsiMax: Number(record.PsiMax ?? record.psi_max ?? 0),
|
psi_max: Number(record.psi_max ?? 0),
|
||||||
PsiLMax: Number(record.PsiLMax ?? record.psi_l_max ?? 0),
|
psi_l_max: Number(record.psi_l_max ?? 0),
|
||||||
Omega: Number(record.Omega ?? record.omega ?? 0),
|
omega: Number(record.omega ?? 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,13 +81,13 @@ async function loadItems() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function recalculate(item: Analize) {
|
async function recalculate(item: Analize) {
|
||||||
recalculatingId.value = item.ID
|
recalculatingId.value = item.id
|
||||||
error.value = ''
|
error.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updated = await recalculateAnalize(item.ID)
|
const updated = await analizeApi.recalculate(item.id)
|
||||||
const normalized = normalizeAnalize(updated)
|
const normalized = normalizeAnalize(updated)
|
||||||
items.value = items.value.map((current) => (current.ID === normalized.ID ? normalized : current))
|
items.value = items.value.map((current) => (current.id === normalized.id ? normalized : current))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = formatApiError(err, 'Не удалось пересчитать analize.')
|
error.value = formatApiError(err, 'Не удалось пересчитать analize.')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -134,32 +134,32 @@ onMounted(loadItems)
|
|||||||
<tr v-if="items.length === 0">
|
<tr v-if="items.length === 0">
|
||||||
<td colspan="8" class="text-center text-muted py-4">Записи не найдены</td>
|
<td colspan="8" class="text-center text-muted py-4">Записи не найдены</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="item in items" :key="item.ID">
|
<tr v-for="item in items" :key="item.id">
|
||||||
<td class="fw-semibold">{{ item.ID }}</td>
|
<td class="fw-semibold">{{ item.id }}</td>
|
||||||
<td>{{ item.Name || '—' }}</td>
|
<td>{{ item.name || '—' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<RouterLink
|
<RouterLink
|
||||||
class="fw-medium text-decoration-none"
|
class="fw-medium text-decoration-none"
|
||||||
:to="{ name: 'control-case-detail', params: { id: item.CaseID } }"
|
:to="{ name: 'control-case-detail', params: { id: item.case_id } }"
|
||||||
>
|
>
|
||||||
{{ item.CaseName || '—' }}
|
{{ item.case_name || '—' }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<small class="text-muted">#{{ item.CaseID }}</small>
|
<small class="text-muted">#{{ item.case_id }}</small>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.PsiMax }}</td>
|
<td>{{ item.psi_max }}</td>
|
||||||
<td>{{ item.PsiLMax }}</td>
|
<td>{{ item.psi_l_max }}</td>
|
||||||
<td>{{ item.Omega }}</td>
|
<td>{{ item.omega }}</td>
|
||||||
<td>{{ formatDate(item.CreatedAt) }}</td>
|
<td>{{ formatDate(item.created_at) }}</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-outline-primary"
|
class="btn btn-sm btn-outline-primary"
|
||||||
type="button"
|
type="button"
|
||||||
@click="recalculate(item)"
|
@click="recalculate(item)"
|
||||||
:disabled="recalculatingId === item.ID"
|
:disabled="recalculatingId === item.id"
|
||||||
>
|
>
|
||||||
{{ recalculatingId === item.ID ? 'Пересчет...' : 'Пересчитать' }}
|
{{ recalculatingId === item.id ? 'Пересчет...' : 'Пересчитать' }}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { RouterLink, useRoute } from 'vue-router'
|
import { RouterLink, useRoute } from 'vue-router'
|
||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
import { getControlCaseChartData, type CsvChartResponse } from '@/api.ts'
|
import { controlCaseApi, type CsvChartResponse } from '@/api.ts'
|
||||||
import { formatApiError } from '@/api_client.ts'
|
import { formatApiError } from '@/api_client.ts'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -61,7 +61,7 @@ async function loadData() {
|
|||||||
error.value = ''
|
error.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
data.value = await getControlCaseChartData(id.value)
|
data.value = await controlCaseApi.chartData(id.value)
|
||||||
renderChart()
|
renderChart()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = formatApiError(err, 'Не удалось загрузить данные графика.')
|
error.value = formatApiError(err, 'Не удалось загрузить данные графика.')
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { RouterLink, useRoute } from 'vue-router'
|
import { RouterLink, useRoute } from 'vue-router'
|
||||||
import { controlCaseApi, recalculateControlCaseAnalysis, createControlCaseInitialCondition } from '@/api.ts'
|
import { analizeApi, controlCaseApi } from '@/api.ts'
|
||||||
import { formatApiError } from '@/api_client.ts'
|
import { formatApiError } from '@/api_client.ts'
|
||||||
import type { ControlCase } from '@/models.ts'
|
import type { ControlCase } from '@/models.ts'
|
||||||
import { controlCaseStatusMeta } from '@/statuses.ts'
|
import { controlCaseStatusMeta } from '@/statuses.ts'
|
||||||
@@ -66,7 +66,7 @@ async function recalculateAnalysis() {
|
|||||||
statusMessage.value = ''
|
statusMessage.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await recalculateControlCaseAnalysis(id.value)
|
await analizeApi.recalculateControlCase(id.value)
|
||||||
statusMessage.value = 'Анализ пересчитан.'
|
statusMessage.value = 'Анализ пересчитан.'
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = formatApiError(err, 'Не удалось пересчитать анализ.')
|
error.value = formatApiError(err, 'Не удалось пересчитать анализ.')
|
||||||
@@ -83,7 +83,7 @@ async function createInitialCondition() {
|
|||||||
statusMessage.value = ''
|
statusMessage.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await createControlCaseInitialCondition(id.value)
|
const response = await controlCaseApi.createInitialCondition(id.value)
|
||||||
statusMessage.value = `Начальные условия созданы: ${response.initial_condition.file_path}`
|
statusMessage.value = `Начальные условия созданы: ${response.initial_condition.file_path}`
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = formatApiError(err, 'Не удалось создать начальные условия.')
|
error.value = formatApiError(err, 'Не удалось создать начальные условия.')
|
||||||
@@ -153,19 +153,19 @@ watch(id, loadItem)
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<dl class="row mb-0">
|
<dl class="row mb-0">
|
||||||
<dt class="col-sm-3">ID</dt>
|
<dt class="col-sm-3">ID</dt>
|
||||||
<dd class="col-sm-9">{{ item.ID }}</dd>
|
<dd class="col-sm-9">{{ item.id }}</dd>
|
||||||
|
|
||||||
<dt class="col-sm-3">Params ID</dt>
|
<dt class="col-sm-3">Params ID</dt>
|
||||||
<dd class="col-sm-9">{{ item.paramsID }}</dd>
|
<dd class="col-sm-9">{{ item.params_id }}</dd>
|
||||||
|
|
||||||
<dt class="col-sm-3">Создан</dt>
|
<dt class="col-sm-3">Создан</dt>
|
||||||
<dd class="col-sm-9">{{ formatDate(item.CreatedAt) }}</dd>
|
<dd class="col-sm-9">{{ formatDate(item.created_at) }}</dd>
|
||||||
|
|
||||||
<dt class="col-sm-3">Обновлён</dt>
|
<dt class="col-sm-3">Обновлён</dt>
|
||||||
<dd class="col-sm-9">{{ formatDate(item.UpdatedAt) }}</dd>
|
<dd class="col-sm-9">{{ formatDate(item.updated_at) }}</dd>
|
||||||
|
|
||||||
<dt class="col-sm-3">Удалён</dt>
|
<dt class="col-sm-3">Удалён</dt>
|
||||||
<dd class="col-sm-9">{{ formatValue(item.DeletedAt) }}</dd>
|
<dd class="col-sm-9">{{ formatValue(item.deleted_at) }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -112,8 +112,8 @@ onMounted(loadItems);
|
|||||||
Записи не найдены
|
Записи не найдены
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="item in items" :key="item.ID">
|
<tr v-for="item in items" :key="item.id">
|
||||||
<td class="fw-semibold">{{ item.ID }}</td>
|
<td class="fw-semibold">{{ item.id }}</td>
|
||||||
<td>{{ item.name || "—" }}</td>
|
<td>{{ item.name || "—" }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span
|
<span
|
||||||
@@ -123,14 +123,14 @@ onMounted(loadItems);
|
|||||||
{{ controlCaseStatusMeta(item.status).label }}
|
{{ controlCaseStatusMeta(item.status).label }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.paramsID }}</td>
|
<td>{{ item.params_id }}</td>
|
||||||
<td>{{ formatDate(item.CreatedAt) }}</td>
|
<td>{{ formatDate(item.created_at) }}</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<RouterLink
|
<RouterLink
|
||||||
class="btn btn-sm btn-primary"
|
class="btn btn-sm btn-primary"
|
||||||
:to="{
|
:to="{
|
||||||
name: 'control-case-detail',
|
name: 'control-case-detail',
|
||||||
params: { id: item.ID },
|
params: { id: item.id },
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
Открыть
|
Открыть
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref } from "vue";
|
import { computed, onMounted, reactive, ref } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { initialConditionApi, launchControlCase, paramsApi } from "@/api.ts";
|
import { controlCaseApi, initialConditionApi, paramsApi } from "@/api.ts";
|
||||||
import { formatApiError, isPaginatedResponse } from "@/api_client.ts";
|
import { formatApiError, isPaginatedResponse } from "@/api_client.ts";
|
||||||
import ParamsEditCard from "@/components/params/ParamsEditCard.vue";
|
import ParamsEditCard from "@/components/params/ParamsEditCard.vue";
|
||||||
import ParamsQuickBatchCard from "@/components/params/ParamsQuickBatchCard.vue";
|
import ParamsQuickBatchCard from "@/components/params/ParamsQuickBatchCard.vue";
|
||||||
@@ -304,7 +304,7 @@ async function createBatch() {
|
|||||||
payload[batchForm.variable] = value;
|
payload[batchForm.variable] = value;
|
||||||
|
|
||||||
const created = await paramsApi.create(payload as never);
|
const created = await paramsApi.create(payload as never);
|
||||||
await launchControlCase({
|
await controlCaseApi.launch({
|
||||||
params_id: created.id,
|
params_id: created.id,
|
||||||
name: `${batchForm.variable}-${value}`,
|
name: `${batchForm.variable}-${value}`,
|
||||||
});
|
});
|
||||||
@@ -324,11 +324,11 @@ async function launchParams(item: Params) {
|
|||||||
error.value = "";
|
error.value = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const controlCase = await launchControlCase({
|
const controlCase = await controlCaseApi.launch({
|
||||||
params_id: item.id,
|
params_id: item.id,
|
||||||
name: `case-${item.id}`,
|
name: `case-${item.id}`,
|
||||||
});
|
});
|
||||||
await router.push({ name: "control-case-detail", params: { id: controlCase.ID } });
|
await router.push({ name: "control-case-detail", params: { id: controlCase.id } });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = formatApiError(err, "Не удалось запустить расчет.");
|
error.value = formatApiError(err, "Не удалось запустить расчет.");
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
+27
-146
@@ -1,18 +1,18 @@
|
|||||||
package control_case
|
package control_case
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/csv"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"control/series"
|
||||||
|
|
||||||
"github.com/che4web/go4rest"
|
"github.com/che4web/go4rest"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gonum.org/v1/gonum/dsp/fourier"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -104,10 +104,7 @@ type CSVSeriesResponse struct {
|
|||||||
Rows [][]interface{} `json:"rows"`
|
Rows [][]interface{} `json:"rows"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FFTPoint struct {
|
type FFTPoint = series.SpectrumPoint
|
||||||
Frequency float64 `json:"frequency"`
|
|
||||||
Amplitude float64 `json:"amplitude"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PSISpectrumResponse struct {
|
type PSISpectrumResponse struct {
|
||||||
TimeStep float64 `json:"time_step"`
|
TimeStep float64 `json:"time_step"`
|
||||||
@@ -132,27 +129,17 @@ func (c *ControlCaseController) ChartData(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
csvPath := controlCase.FooCSVPath()
|
csvPath := controlCase.FooCSVPath()
|
||||||
file, err := os.Open(csvPath)
|
data, ok := readCSVResponse(ctx, csvPath)
|
||||||
if err != nil {
|
if !ok {
|
||||||
ctx.JSON(http.StatusNotFound, gin.H{"error": "csv file not found"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer file.Close()
|
if len(data.Columns) == 0 {
|
||||||
|
|
||||||
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{}{}})
|
ctx.JSON(http.StatusOK, CSVSeriesResponse{Columns: []string{}, Rows: [][]interface{}{}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
columns := records[0]
|
rows := make([][]interface{}, 0, len(data.Rows))
|
||||||
rows := make([][]interface{}, 0, len(records)-1)
|
for _, record := range data.Rows {
|
||||||
for _, record := range records[1:] {
|
|
||||||
row := make([]interface{}, 0, len(record))
|
row := make([]interface{}, 0, len(record))
|
||||||
for _, value := range record {
|
for _, value := range record {
|
||||||
trimmed := strings.TrimSpace(value)
|
trimmed := strings.TrimSpace(value)
|
||||||
@@ -165,7 +152,7 @@ func (c *ControlCaseController) ChartData(ctx *gin.Context) {
|
|||||||
rows = append(rows, row)
|
rows = append(rows, row)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.JSON(http.StatusOK, CSVSeriesResponse{Columns: columns, Rows: rows})
|
ctx.JSON(http.StatusOK, CSVSeriesResponse{Columns: data.Columns, Rows: rows})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ControlCaseController) PSISpectrum(ctx *gin.Context) {
|
func (c *ControlCaseController) PSISpectrum(ctx *gin.Context) {
|
||||||
@@ -182,80 +169,23 @@ func (c *ControlCaseController) PSISpectrum(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
csvPath := controlCase.FooCSVPath()
|
csvPath := controlCase.FooCSVPath()
|
||||||
file, err := os.Open(csvPath)
|
data, ok := readCSVResponse(ctx, csvPath)
|
||||||
if err != nil {
|
if !ok {
|
||||||
ctx.JSON(http.StatusNotFound, gin.H{"error": "csv file not found"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer file.Close()
|
if len(data.Rows) == 0 {
|
||||||
|
|
||||||
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{}})
|
ctx.JSON(http.StatusOK, PSISpectrumResponse{Points: map[string][]FFTPoint{}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
headers := make(map[string]int, len(records[0]))
|
columns, err := data.NumericColumns("t", "psi_m", "psi_l")
|
||||||
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 {
|
if err != nil {
|
||||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
times, psiM, psiL := columns[0], columns[1], columns[2]
|
||||||
|
|
||||||
times := make([]float64, 0, len(records)-1)
|
timeStep := series.AverageDelta(times)
|
||||||
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 {
|
if timeStep <= 0 {
|
||||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid time step"})
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid time step"})
|
||||||
return
|
return
|
||||||
@@ -264,72 +194,23 @@ func (c *ControlCaseController) PSISpectrum(ctx *gin.Context) {
|
|||||||
ctx.JSON(http.StatusOK, PSISpectrumResponse{
|
ctx.JSON(http.StatusOK, PSISpectrumResponse{
|
||||||
TimeStep: timeStep,
|
TimeStep: timeStep,
|
||||||
Points: map[string][]FFTPoint{
|
Points: map[string][]FFTPoint{
|
||||||
"psi_m": buildSpectrumPoints(psiM, timeStep),
|
"psi_m": series.SpectrumPoints(psiM, timeStep),
|
||||||
"psi_l": buildSpectrumPoints(psiL, timeStep),
|
"psi_l": series.SpectrumPoints(psiL, timeStep),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildSpectrumPoints(values []float64, dt float64) []FFTPoint {
|
func readCSVResponse(ctx *gin.Context, csvPath string) (*series.CSVData, bool) {
|
||||||
if len(values) < 2 || dt <= 0 {
|
data, err := series.ReadCSV(csvPath)
|
||||||
return []FFTPoint{}
|
if err == nil {
|
||||||
|
return data, true
|
||||||
}
|
}
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
centered := make([]float64, len(values))
|
ctx.JSON(http.StatusNotFound, gin.H{"error": "csv file not found"})
|
||||||
mean := 0.0
|
} else {
|
||||||
for _, v := range values {
|
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
mean += v
|
|
||||||
}
|
}
|
||||||
mean /= float64(len(values))
|
return nil, false
|
||||||
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) {
|
func (c *ControlCaseController) FieldMap(ctx *gin.Context) {
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ControlCase struct {
|
type ControlCase struct {
|
||||||
gorm.Model
|
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"`
|
Name string `json:"name"`
|
||||||
ParamsID uint `json:"params_id"`
|
ParamsID uint `json:"params_id"`
|
||||||
Params Params `json:"params"`
|
Params Params `json:"params"`
|
||||||
@@ -59,7 +62,10 @@ type Params struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type InitialCondition struct {
|
type InitialCondition struct {
|
||||||
gorm.Model
|
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"`
|
Name string `json:"name"`
|
||||||
FilePath string `json:"file_path"`
|
FilePath string `json:"file_path"`
|
||||||
}
|
}
|
||||||
|
|||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
package series
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CSVData struct {
|
||||||
|
Columns []string
|
||||||
|
Rows [][]string
|
||||||
|
index map[string]int
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadCSV(path string) (*CSVData, error) {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
records, err := csv.NewReader(file).ReadAll()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(records) == 0 {
|
||||||
|
return &CSVData{Columns: []string{}, Rows: [][]string{}, index: map[string]int{}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data := &CSVData{
|
||||||
|
Columns: records[0],
|
||||||
|
Rows: records[1:],
|
||||||
|
index: make(map[string]int, len(records[0])),
|
||||||
|
}
|
||||||
|
for i, column := range data.Columns {
|
||||||
|
data.index[strings.ToLower(strings.TrimSpace(column))] = i
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *CSVData) Index(name string) (int, error) {
|
||||||
|
idx, ok := d.index[strings.ToLower(name)]
|
||||||
|
if !ok {
|
||||||
|
return -1, fmt.Errorf("missing column %q", name)
|
||||||
|
}
|
||||||
|
return idx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *CSVData) NumericColumns(names ...string) ([][]float64, error) {
|
||||||
|
indices := make([]int, len(names))
|
||||||
|
for i, name := range names {
|
||||||
|
idx, err := d.Index(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
indices[i] = idx
|
||||||
|
}
|
||||||
|
|
||||||
|
values := make([][]float64, len(names))
|
||||||
|
for i := range values {
|
||||||
|
values[i] = make([]float64, 0, len(d.Rows))
|
||||||
|
}
|
||||||
|
|
||||||
|
maxIndex := MaxInt(indices...)
|
||||||
|
for _, row := range d.Rows {
|
||||||
|
if len(row) <= maxIndex {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed := make([]float64, len(indices))
|
||||||
|
valid := true
|
||||||
|
for i, idx := range indices {
|
||||||
|
value, err := ParseFloat(row[idx])
|
||||||
|
if err != nil {
|
||||||
|
valid = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
parsed[i] = value
|
||||||
|
}
|
||||||
|
if !valid {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, value := range parsed {
|
||||||
|
values[i] = append(values[i], value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return values, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseFloat(value string) (float64, error) {
|
||||||
|
return strconv.ParseFloat(strings.TrimSpace(value), 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MaxInt(values ...int) int {
|
||||||
|
max := 0
|
||||||
|
for _, value := range values {
|
||||||
|
if value > max {
|
||||||
|
max = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
|
||||||
|
func MaxLastHalf(values []float64) float64 {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return slices.Max(values[len(values)/2:])
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package series
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"gonum.org/v1/gonum/dsp/fourier"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SpectrumPoint struct {
|
||||||
|
Frequency float64 `json:"frequency"`
|
||||||
|
Amplitude float64 `json:"amplitude"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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 SpectrumPoints(values []float64, dt float64) []SpectrumPoint {
|
||||||
|
if len(values) < 2 || dt <= 0 {
|
||||||
|
return []SpectrumPoint{}
|
||||||
|
}
|
||||||
|
|
||||||
|
centered := Centered(values)
|
||||||
|
fft := fourier.NewFFT(len(centered))
|
||||||
|
coeffs := fft.Coefficients(nil, centered)
|
||||||
|
limit := len(coeffs) / 2
|
||||||
|
points := make([]SpectrumPoint, 0, limit+1)
|
||||||
|
for i := 0; i <= limit; i++ {
|
||||||
|
points = append(points, SpectrumPoint{
|
||||||
|
Frequency: float64(i) / (float64(len(centered)) * dt),
|
||||||
|
Amplitude: math.Hypot(real(coeffs[i]), imag(coeffs[i])) / float64(len(centered)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return points
|
||||||
|
}
|
||||||
|
|
||||||
|
func MainAngularFrequency(times, values []float64) float64 {
|
||||||
|
if len(values) < 2 || len(times) < 2 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
dt := AverageDelta(times)
|
||||||
|
if dt <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
centered := Centered(values)
|
||||||
|
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 := math.Hypot(real(coeffs[i]), imag(coeffs[i]))
|
||||||
|
if amp > bestAmp {
|
||||||
|
bestAmp = amp
|
||||||
|
bestIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
frequencyHz := fft.Freq(bestIndex) / dt
|
||||||
|
return 2 * math.Pi * frequencyHz
|
||||||
|
}
|
||||||
|
|
||||||
|
func Centered(values []float64) []float64 {
|
||||||
|
centered := make([]float64, len(values))
|
||||||
|
mean := 0.0
|
||||||
|
for _, value := range values {
|
||||||
|
mean += value
|
||||||
|
}
|
||||||
|
mean /= float64(len(values))
|
||||||
|
for i, value := range values {
|
||||||
|
centered[i] = value - mean
|
||||||
|
}
|
||||||
|
return centered
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user