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