'#c62828', 'amber' => '#f9a825', 'green' => '#2e7d32']; private function db() { return DB::connection('softspm'); } // ===================== LOOKUPY / ZAPIS ===================== /** * Dodanie pompy. Użytkownik podaje H i η dla każdego z 8 punktów Q * (Q generowane z Q_n). Wszystko transakcyjnie. * * @param array $data name, manufacturer_id, q_n, length, diameter, weight, * h: float[8], eta: float[8] (w kolejności rosnącego Q) */ public function create(array $data): Pump { return $this->db()->transaction(function () use ($data) { $qn = (float) $data['q_n']; $h = array_values($data['h']); $eta = array_values($data['eta']); $pump = Pump::create([ 'PUP_MCF_ID' => $data['manufacturer_id'], 'PUP_NAME' => $data['name'], 'PUP_Q_N' => $qn, 'PUP_Q_MIN' => 0.55 * $qn, 'PUP_Q_MAX' => 1.35 * $qn, 'PUP_H_P' => $h[self::NOMINAL_INDEX] ?? null, 'PUP_H_MAX' => $h[2] ?? null, 'PUP_H_MIN' => $h[6] ?? null, 'PUP_ETA_MAX' => $eta[self::NOMINAL_INDEX] ?? null, 'PUP_PUMP_LENGTH' => $data['length'] ?? null, 'PUP_WEIGHT' => $data['weight'] ?? null, 'PUP_DIAMETER' => $data['diameter'] ?? null, 'PUP_STATUS' => '1', ]); $rows = []; foreach (self::Q_FACTORS as $i => $f) { $x = round($f * $qn, 4); $rows[] = ['PEP_PUP_ID' => $pump->PUP_ID, 'PEP_X_PARAM' => $x, 'PEP_Y_PARAM' => (float) ($h[$i] ?? 0), 'PEP_TYPE' => 'H']; $rows[] = ['PEP_PUP_ID' => $pump->PUP_ID, 'PEP_X_PARAM' => $x, 'PEP_Y_PARAM' => (float) ($eta[$i] ?? 0), 'PEP_TYPE' => 'ETA']; } PumpCurvePoint::insert($rows); return $pump; }); } /** * Aktualizacja pompy: dane + wartości Y krzywych (po PEP_ID) + pochodne * (3. punkt H=H_MAX, 5.=H_P, 7.=H_MIN; 5. ETA=ETA_MAX) - jak legacy. */ public function update(Pump $pump, array $data): void { $this->db()->transaction(function () use ($pump, $data) { $pump->update([ 'PUP_MCF_ID' => $data['manufacturer_id'], 'PUP_NAME' => $data['name'], 'PUP_PUMP_LENGTH' => $data['length'] ?? null, 'PUP_WEIGHT' => $data['weight'] ?? null, 'PUP_DIAMETER' => $data['diameter'] ?? null, ]); $hPoints = $pump->curvePoints()->where('PEP_TYPE', 'H')->orderBy('PEP_X_PARAM')->get(); foreach ($hPoints->values() as $idx => $point) { $y = $this->num($data['h'][$point->PEP_ID] ?? $point->PEP_Y_PARAM); $point->update(['PEP_Y_PARAM' => $y]); match ($idx + 1) { 3 => $pump->update(['PUP_H_MAX' => $y]), 5 => $pump->update(['PUP_H_P' => $y]), 7 => $pump->update(['PUP_H_MIN' => $y]), default => null, }; } $etaPoints = $pump->curvePoints()->where('PEP_TYPE', 'ETA')->orderBy('PEP_X_PARAM')->get(); foreach ($etaPoints->values() as $idx => $point) { $y = $this->num($data['eta'][$point->PEP_ID] ?? $point->PEP_Y_PARAM); $point->update(['PEP_Y_PARAM' => $y]); if ($idx + 1 === 5) { $pump->update(['PUP_ETA_MAX' => $y]); } } }); } /** Usunięcie logiczne (PUP_STATUS='0'). */ public function delete(Pump $pump): void { $pump->update(['PUP_STATUS' => '0']); } // ===================== ODCZYT KRZYWYCH ===================== /** Punkty krzywej danego typu: kolekcja {id,x,y} (x = PEP_X_PARAM, m³/h). */ public function curve(Pump $pump, string $type): Collection { return $pump->curvePoints() ->where('PEP_TYPE', $type) ->orderBy('PEP_X_PARAM') ->get() ->map(fn ($p) => (object) [ 'id' => $p->PEP_ID, 'x' => round((float) $p->PEP_X_PARAM, 3), 'y' => (float) $p->PEP_Y_PARAM, ]) ->values(); } /** * Czy krzywą można narysować - jak legacy: * H → 0 wartości Y=0, * ETA → <2 wartości Y=0 (punkt przy Q=0 legalnie =0). */ public function curveDrawable(Collection $points, int $allowedZeros): bool { if ($points->isEmpty()) { return false; } $zeros = $points->filter(fn ($p) => (float) $p->y == 0.0)->count(); return $zeros <= $allowedZeros; } /** Wartość z aproksymacji wielomianowej (4 współcz.) dla zadanego Q. */ public function valueAt(Collection $points, float $q): ?float { if ($points->isEmpty()) { return null; } $reg = new PolynomialRegression(4); foreach ($points as $p) { $reg->addData((float) $p->x, (float) $p->y); } return $reg->interpolate($reg->getCoefficients(), $q); } /** * Model wykresu: aproksymacja wielomianowa próbkowana i pocięta na * segmenty kolorowane wg stref Q (scope[0..6]) - wiernie wg legacy: * [0→scope2] red, [scope2→scope3] amber, [scope3→scope5] green, * [scope5→scope6] amber, [scope6→max] red. Plus surowe punkty (niebieskie). * * @return array{segments:array}>,raw:Collection,xMax:float,yMax:float} */ public function plot(Collection $points): array { $scope = $points->pluck('x')->map(fn ($v) => (float) $v)->values()->all(); $xMax = max($points->max('x'), 0.0001); $reg = new PolynomialRegression(4); foreach ($points as $p) { $reg->addData((float) $p->x, (float) $p->y); } $coeffs = $reg->getCoefficients(); $band = function (float $x) use ($scope): string { if (count($scope) < 7) { return 'green'; } if ($x < $scope[2]) return 'red'; if ($x < $scope[3]) return 'amber'; if ($x < $scope[5]) return 'green'; if ($x < $scope[6]) return 'amber'; return 'red'; }; $n = 120; $segments = []; $current = null; $yMax = (float) $points->max('y'); $prev = null; for ($i = 0; $i <= $n; $i++) { $x = $xMax * $i / $n; $y = max(0.0, $reg->interpolate($coeffs, $x)); $yMax = max($yMax, $y); $color = self::BAND_COLORS[$band($x)]; $pt = ['x' => $x, 'y' => $y]; if ($current === null || $current['color'] !== $color) { if ($current !== null) { $current['points'][] = $pt; // domknij łączeniem $segments[] = $current; } $current = ['color' => $color, 'points' => $prev !== null ? [$prev, $pt] : [$pt]]; } else { $current['points'][] = $pt; } $prev = $pt; } if ($current !== null) { $segments[] = $current; } return [ 'segments' => $segments, 'raw' => $points, 'xMax' => $xMax, 'yMax' => max($yMax, 0.0001) * 1.08, 'coeffs' => array_values($coeffs), // [c0,c1,c2,c3] do interaktywnego markera ]; } private function num(mixed $v): float { return (float) str_replace(',', '.', (string) $v); } // ===================== DOBÓR AGREGATU (site201) ===================== /** * Dobór agregatu pompowego (legacy site201): pompy, których zakres pracy * obejmuje zadane Q (PUP_Q_MIN < Q < PUP_Q_MAX; skala UAT bez ×60). Dla * każdej liczy H(Q) i η(Q) z aproksymacji wielomianowej (st.4). Gdy podano * H, filtruje do pomp, dla których zadane H mieści się w tolerancji legacy * [H(Q)-5, H(Q)+8]. Wynik sortowany malejąco po sprawności η(Q). * * @return Collection każdy: {pump, h_val, eta_val, plot|null} */ public function matchingAggregates(float $q, ?float $h): Collection { return Pump::query()->visible()->with('manufacturer') ->where('PUP_Q_MIN', '<', $q) ->where('PUP_Q_MAX', '>', $q) ->orderBy('PUP_NAME') ->get() ->map(function (Pump $pump) use ($q) { $hCurve = $this->curve($pump, 'H'); $hVal = $this->valueAt($hCurve, $q); if ($hVal === null) { return null; // brak krzywej H - pompy nie da się ocenić } return (object) [ 'pump' => $pump, 'h_val' => $hVal, 'eta_val' => $this->valueAt($this->curve($pump, 'ETA'), $q), 'plot' => $this->curveDrawable($hCurve, 0) ? $this->plot($hCurve) : null, ]; }) ->filter() ->when($h !== null, fn (Collection $c) => $c->filter( fn ($m) => $h >= $m->h_val - 5 && $h <= $m->h_val + 8 )) ->sortByDesc('eta_val') ->values(); } }