277 lines
10 KiB
PHP
277 lines
10 KiB
PHP
<?php
|
||
|
||
namespace App\Services;
|
||
|
||
use App\Models\Pump;
|
||
use App\Models\PumpCurvePoint;
|
||
use App\Support\PolynomialRegression;
|
||
use Illuminate\Support\Collection;
|
||
use Illuminate\Support\Facades\DB;
|
||
|
||
/**
|
||
* Logika domenowa pomp (legacy site203 / site203inf) - funkcjonalność 1:1.
|
||
*
|
||
* SKALA: baza UAT trzyma Q/krzywe BEZ ×60 (PUP_Q_N, PEP_X_PARAM w m³/h) -
|
||
* nie dzielimy/mnożymy przez 60.
|
||
*
|
||
* Krzywe Q-H i Q-η: 8 punktów przy stałych ułamkach Q_n (indeks 4 = nominał).
|
||
*/
|
||
class PumpService
|
||
{
|
||
private const Q_FACTORS = [0.0, 0.25, 0.55, 0.7, 1.0, 1.22, 1.35, 1.42];
|
||
private const NOMINAL_INDEX = 4;
|
||
|
||
/** Kolory segmentów krzywej (wg algorytmu legacy). */
|
||
private const BAND_COLORS = ['red' => '#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<int,array{color:string,points:array<int,array{x:float,y:float}>}>,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<int,object> 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();
|
||
}
|
||
}
|