Files
kopalnia_2026/app/Services/PumpService.php
T
2026-09-03 12:21:17 +02:00

277 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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();
}
}