Initial commit

This commit is contained in:
2026-09-03 12:21:17 +02:00
commit 6d40da0410
155 changed files with 21554 additions and 0 deletions
+276
View File
@@ -0,0 +1,276 @@
<?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();
}
}