Compare commits
16 Commits
c45679d1f7
...
linux
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d751861c8 | |||
|
|
d2a9a4197c | ||
|
|
6ee68aeaed | ||
|
|
d22184cf70 | ||
|
|
24db8ad426 | ||
|
|
c0c0e6e3ea | ||
|
|
60f951a823 | ||
|
|
ec73a755c2 | ||
|
|
1c2f622a2f | ||
|
|
0f6df68f7c | ||
|
|
d48d7828f5 | ||
|
|
6ff041d2a8 | ||
|
|
59c4e3857a | ||
|
|
68f061e996 | ||
|
|
5f3d2a78d7 | ||
|
|
ee8a10d875 |
14
.dockerignore
Normal file
14
.dockerignore
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
__pycache__/
|
||||||
|
.venv/
|
||||||
|
test_outputs/
|
||||||
|
tests/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
.pytest_cache/
|
||||||
|
.git/
|
||||||
|
tests_notebook/
|
||||||
|
IPM_DLL/
|
||||||
|
IPM_SO/
|
||||||
|
*.xlsm
|
||||||
|
datasets-2025-10-18-14-21.csv
|
||||||
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -2,7 +2,6 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
*.so
|
|
||||||
*.egg
|
*.egg
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
dist/
|
dist/
|
||||||
@@ -53,6 +52,5 @@ htmlcov/
|
|||||||
*.bak
|
*.bak
|
||||||
*.swp
|
*.swp
|
||||||
|
|
||||||
# Ignore platform native binaries copied into app/ipm/lib
|
# Ignore platform native binaries copied into app/ipm/lib (Windows dlls still ignored)
|
||||||
app/ipm/lib/windows/*.dll
|
app/ipm/lib/windows/*.dll
|
||||||
app/ipm/lib/linux/*.so
|
|
||||||
43
Dockerfile
Normal file
43
Dockerfile
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system deps required by numpy/pandas/matplotlib and building wheels
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
gcc \
|
||||||
|
gfortran \
|
||||||
|
libatlas3-base \
|
||||||
|
libopenblas-dev \
|
||||||
|
liblapack-dev \
|
||||||
|
libfreetype6-dev \
|
||||||
|
libpng-dev \
|
||||||
|
pkg-config \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
git \
|
||||||
|
libglib2.0-0 \
|
||||||
|
libxrender1 \
|
||||||
|
libxext6 \
|
||||||
|
libsm6 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy only requirements first for better layer caching
|
||||||
|
COPY requirements.txt /app/requirements.txt
|
||||||
|
|
||||||
|
# Upgrade pip and install python deps
|
||||||
|
RUN python -m pip install --upgrade pip setuptools wheel && \
|
||||||
|
python -m pip install -r /app/requirements.txt
|
||||||
|
|
||||||
|
# Copy project
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
# Ensure Python and dynamic linker will find the native libs if mounted
|
||||||
|
ENV PYTHONPATH="/app:/app/IPM_SO:/app/IPM_DLL"
|
||||||
|
ENV LD_LIBRARY_PATH="/app/IPM_SO:/app/IPM_DLL"
|
||||||
|
|
||||||
|
EXPOSE 8001
|
||||||
|
|
||||||
|
# Default command runs uvicorn (use docker-compose override for development)
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8001"]
|
||||||
0
Frontend/.dockerignore
Normal file
0
Frontend/.dockerignore
Normal file
0
Frontend/Dockerfile
Normal file
0
Frontend/Dockerfile
Normal file
15
Frontend/next.config.js
Normal file
15
Frontend/next.config.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
output: 'standalone',
|
||||||
|
async rewrites() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/api/v1/:path*',
|
||||||
|
destination: process.env.NEXT_PUBLIC_API_URL || 'http://backend:8001/api/v1/:path*',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
||||||
3213
Frontend/package-lock.json
generated
Normal file
3213
Frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
Frontend/package.json
Normal file
36
Frontend/package.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "diagram-ph-frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^5.2.2",
|
||||||
|
"@types/node": "^24.8.1",
|
||||||
|
"@types/react": "^19.2.2",
|
||||||
|
"axios": "^1.12.2",
|
||||||
|
"lucide-react": "^0.546.0",
|
||||||
|
"next": "^15.5.6",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0",
|
||||||
|
"react-hook-form": "^7.65.0",
|
||||||
|
"recharts": "^3.3.0",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"zod": "^4.1.12"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"tailwindcss": "^3.4.18"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Frontend/public/sample_test_data.csv
Normal file
11
Frontend/public/sample_test_data.csv
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Pressure (bar),Enthalpy (kJ/kg)
|
||||||
|
3.2,580
|
||||||
|
3.5,595
|
||||||
|
11.8,650
|
||||||
|
12.1,652
|
||||||
|
12.0,380
|
||||||
|
11.9,375
|
||||||
|
3.3,378
|
||||||
|
8.5,500
|
||||||
|
5.0,450
|
||||||
|
7.2,520
|
||||||
|
16
Frontend/public/sample_test_data.json
Normal file
16
Frontend/public/sample_test_data.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"description": "Sample test points for R290 refrigeration cycle",
|
||||||
|
"refrigerant": "R290",
|
||||||
|
"points": [
|
||||||
|
{ "pressure": 3.2, "enthalpy": 580 },
|
||||||
|
{ "pressure": 3.5, "enthalpy": 595 },
|
||||||
|
{ "pressure": 11.8, "enthalpy": 650 },
|
||||||
|
{ "pressure": 12.1, "enthalpy": 652 },
|
||||||
|
{ "pressure": 12.0, "enthalpy": 380 },
|
||||||
|
{ "pressure": 11.9, "enthalpy": 375 },
|
||||||
|
{ "pressure": 3.3, "enthalpy": 378 },
|
||||||
|
{ "pressure": 8.5, "enthalpy": 500 },
|
||||||
|
{ "pressure": 5.0, "enthalpy": 450 },
|
||||||
|
{ "pressure": 7.2, "enthalpy": 520 }
|
||||||
|
]
|
||||||
|
}
|
||||||
30
Frontend/src/app/globals.css
Normal file
30
Frontend/src/app/globals.css
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||||
|
background: #0a0e1a;
|
||||||
|
min-height: 100vh;
|
||||||
|
color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #0a0e1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #2d3548;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #3d4558;
|
||||||
|
}
|
||||||
19
Frontend/src/app/layout.tsx
Normal file
19
Frontend/src/app/layout.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { Metadata } from 'next'
|
||||||
|
import './globals.css'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Diagram PH - Refrigeration Cycle Calculator',
|
||||||
|
description: 'Calculate and visualize refrigeration cycles',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
67
Frontend/src/app/page.tsx
Normal file
67
Frontend/src/app/page.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import PHDiagramModern from "@/components/PHDiagramModern";
|
||||||
|
import CycleCalculatorModern from "@/components/CycleCalculatorModern";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const [activeView, setActiveView] = useState<'calculator' | 'diagram'>('diagram');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: '2rem',
|
||||||
|
right: '2rem',
|
||||||
|
zIndex: 1000,
|
||||||
|
display: 'flex',
|
||||||
|
gap: '0.75rem',
|
||||||
|
background: 'rgba(30, 58, 95, 0.95)',
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
padding: '0.5rem',
|
||||||
|
borderRadius: '16px',
|
||||||
|
boxShadow: '0 8px 32px rgba(0,0,0,0.3)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.1)'
|
||||||
|
}}>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveView('diagram')}
|
||||||
|
style={{
|
||||||
|
padding: '0.75rem 1.5rem',
|
||||||
|
background: activeView === 'diagram' ? 'linear-gradient(135deg, #38b2ac 0%, #2c7a7b 100%)' : 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '12px',
|
||||||
|
color: '#ffffff',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
fontWeight: '700',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.3s',
|
||||||
|
boxShadow: activeView === 'diagram' ? '0 4px 12px rgba(56, 178, 172, 0.4)' : 'none',
|
||||||
|
letterSpacing: '0.3px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
📊 P-h Diagram
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveView('calculator')}
|
||||||
|
style={{
|
||||||
|
padding: '0.75rem 1.5rem',
|
||||||
|
background: activeView === 'calculator' ? 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)' : 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '12px',
|
||||||
|
color: '#ffffff',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
fontWeight: '700',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.3s',
|
||||||
|
boxShadow: activeView === 'calculator' ? '0 4px 12px rgba(59, 130, 246, 0.4)' : 'none',
|
||||||
|
letterSpacing: '0.3px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
🧮 Cycle Calculator
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{activeView === 'diagram' && <PHDiagramModern />}
|
||||||
|
{activeView === 'calculator' && <CycleCalculatorModern />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
1067
Frontend/src/components/CycleCalculatorModern.tsx
Normal file
1067
Frontend/src/components/CycleCalculatorModern.tsx
Normal file
File diff suppressed because it is too large
Load Diff
1498
Frontend/src/components/PHDiagramModern.tsx
Normal file
1498
Frontend/src/components/PHDiagramModern.tsx
Normal file
File diff suppressed because it is too large
Load Diff
65
Frontend/src/lib/api-client.ts
Normal file
65
Frontend/src/lib/api-client.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import {
|
||||||
|
CycleCalculationRequest,
|
||||||
|
CycleCalculationResponse,
|
||||||
|
DiagramRequest,
|
||||||
|
DiagramResponse,
|
||||||
|
RefrigerantInfo,
|
||||||
|
RefrigerantsListResponse,
|
||||||
|
} from '@/types/api';
|
||||||
|
|
||||||
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8001/api/v1';
|
||||||
|
|
||||||
|
export class ApiClient {
|
||||||
|
private baseUrl: string;
|
||||||
|
|
||||||
|
constructor(baseUrl: string = API_BASE_URL) {
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRefrigerants(): Promise<RefrigerantInfo[]> {
|
||||||
|
const response = await fetch(`${this.baseUrl}/refrigerants/`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch refrigerants');
|
||||||
|
}
|
||||||
|
const data: RefrigerantsListResponse = await response.json();
|
||||||
|
return data.refrigerants;
|
||||||
|
}
|
||||||
|
|
||||||
|
async calculateCycle(
|
||||||
|
request: CycleCalculationRequest
|
||||||
|
): Promise<CycleCalculationResponse> {
|
||||||
|
const response = await fetch(`${this.baseUrl}/cycles/simple`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.detail || 'Failed to calculate cycle');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateDiagram(request: DiagramRequest): Promise<DiagramResponse> {
|
||||||
|
const response = await fetch(`${this.baseUrl}/diagrams/ph`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.detail || 'Failed to generate diagram');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apiClient = new ApiClient();
|
||||||
84
Frontend/src/types/api.ts
Normal file
84
Frontend/src/types/api.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
export interface CycleCalculationRequest {
|
||||||
|
refrigerant: string;
|
||||||
|
evap_temperature?: number;
|
||||||
|
cond_temperature?: number;
|
||||||
|
evap_pressure?: number;
|
||||||
|
cond_pressure?: number;
|
||||||
|
compressor_efficiency?: number;
|
||||||
|
superheat?: number;
|
||||||
|
subcool?: number;
|
||||||
|
mass_flow?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CyclePoint {
|
||||||
|
point_id: string;
|
||||||
|
pressure: number;
|
||||||
|
temperature?: number;
|
||||||
|
enthalpy?: number;
|
||||||
|
entropy?: number;
|
||||||
|
quality?: number;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CyclePerformance {
|
||||||
|
cop: number;
|
||||||
|
cooling_capacity: number;
|
||||||
|
heating_capacity: number;
|
||||||
|
compressor_power: number;
|
||||||
|
compressor_efficiency: number;
|
||||||
|
mass_flow: number;
|
||||||
|
volumetric_flow?: number;
|
||||||
|
compression_ratio: number;
|
||||||
|
discharge_temperature: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CycleCalculationResponse {
|
||||||
|
success: boolean;
|
||||||
|
refrigerant: string;
|
||||||
|
points: CyclePoint[];
|
||||||
|
performance: CyclePerformance;
|
||||||
|
diagram_data?: any;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiagramRequest {
|
||||||
|
refrigerant: string;
|
||||||
|
pressure_range: {
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
};
|
||||||
|
enthalpy_range?: {
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
};
|
||||||
|
include_isotherms?: boolean;
|
||||||
|
isotherm_values?: number[];
|
||||||
|
cycle_points?: Array<{ enthalpy: number; pressure: number }>;
|
||||||
|
title?: string;
|
||||||
|
format?: string;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
dpi?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiagramResponse {
|
||||||
|
success: boolean;
|
||||||
|
image?: string;
|
||||||
|
data?: any;
|
||||||
|
metadata: any;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefrigerantInfo {
|
||||||
|
name: string;
|
||||||
|
formula?: string;
|
||||||
|
available: boolean;
|
||||||
|
loaded?: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefrigerantsListResponse {
|
||||||
|
refrigerants: RefrigerantInfo[];
|
||||||
|
total: number;
|
||||||
|
available_count: number;
|
||||||
|
}
|
||||||
24
Frontend/tsconfig.json
Normal file
24
Frontend/tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [{ "name": "next" }],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
0
README-DOCKER.md
Normal file
0
README-DOCKER.md
Normal file
116
README.md
116
README.md
@@ -41,113 +41,29 @@ graph TB
|
|||||||
F[CloudFront CDN]
|
F[CloudFront CDN]
|
||||||
G[Application Load Balancer]
|
G[Application Load Balancer]
|
||||||
|
|
||||||
subgraph "Elastic Beanstalk Environment"
|
# API Diagramme PH - Project Overview (English)
|
||||||
H1[API Server 1<br/>Docker Container]
|
|
||||||
H2[API Server 2<br/>Docker Container]
|
|
||||||
H3[API Server N<br/>Docker Container]
|
|
||||||
end
|
|
||||||
|
|
||||||
I[CloudWatch<br/>Logs & Metrics]
|
This repository contains a FastAPI-based REST API for generating Pressure-Enthalpy (PH) diagrams
|
||||||
J[S3 Bucket<br/>Static Assets]
|
and performing advanced refrigeration thermodynamic calculations.
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "API Container"
|
For the full French documentation, see: `README_fr.md` (converted from the original README).
|
||||||
K[FastAPI Application]
|
|
||||||
L[RefrigerantEngine<br/>DLL/SO Wrapper]
|
|
||||||
M[DiagramGenerator<br/>Matplotlib/Plotly]
|
|
||||||
N[CycleCalculator<br/>Thermodynamics]
|
|
||||||
O[Cache Layer<br/>LRU + TTL]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "Native Libraries"
|
Badges
|
||||||
P[R134a.so]
|
- Python 3.12+
|
||||||
Q[R410A.so]
|
- FastAPI
|
||||||
R[refifc.so]
|
- Docker-ready
|
||||||
S[Other refrigerants...]
|
|
||||||
end
|
|
||||||
|
|
||||||
A & B & C & D --> E
|
Quick start
|
||||||
E --> F
|
- Install dependencies and run with uvicorn (see documentation in the `docs/` folder).
|
||||||
F --> G
|
|
||||||
G --> H1 & H2 & H3
|
|
||||||
H1 & H2 & H3 --> I
|
|
||||||
H1 & H2 & H3 -.-> J
|
|
||||||
|
|
||||||
H1 --> K
|
Repository structure (short)
|
||||||
K --> L & M & N & O
|
- `app/` : application code
|
||||||
L --> P & Q & R & S
|
- `libs/` : native libraries (dll/ and so/)
|
||||||
|
- `scripts/` : helper scripts
|
||||||
|
- `docs/` : extra documentation
|
||||||
|
|
||||||
style A fill:#e1f5ff
|
If you need the original French README, open `README_fr.md`.
|
||||||
style B fill:#e1f5ff
|
|
||||||
style C fill:#e1f5ff
|
|
||||||
style D fill:#e1f5ff
|
|
||||||
style G fill:#ff9999
|
|
||||||
style H1 fill:#99ff99
|
|
||||||
style H2 fill:#99ff99
|
|
||||||
style H3 fill:#99ff99
|
|
||||||
style K fill:#ffcc99
|
|
||||||
style L fill:#ffff99
|
|
||||||
style M fill:#ffff99
|
|
||||||
style N fill:#ffff99
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📁 Structure du projet
|
|
||||||
|
|
||||||
```
|
|
||||||
diagram-ph-api/
|
|
||||||
├── 📄 API_SPECIFICATION.md # Spécifications complètes des endpoints
|
|
||||||
├── 📄 ARCHITECTURE.md # Architecture technique détaillée
|
|
||||||
├── 📄 DEPLOYMENT.md # Guide de déploiement AWS
|
|
||||||
├── 📄 IMPLEMENTATION_PLAN.md # Plan d'implémentation par phases
|
|
||||||
├── 📄 README.md # Ce fichier
|
|
||||||
│
|
|
||||||
├── app/ # Code source de l'API
|
|
||||||
│ ├── main.py # Point d'entrée FastAPI
|
|
||||||
│ ├── config.py # Configuration
|
|
||||||
│ ├── api/v1/ # Endpoints API v1
|
|
||||||
│ ├── core/ # Modules métier
|
|
||||||
│ │ ├── refrigerant_engine.py
|
|
||||||
│ │ ├── diagram_generator.py
|
|
||||||
│ │ ├── cycle_calculator.py
|
|
||||||
│ │ └── economizer.py
|
|
||||||
│ ├── models/ # Modèles Pydantic
|
|
||||||
│ ├── services/ # Business logic
|
|
||||||
│ └── utils/ # Utilitaires
|
|
||||||
│
|
|
||||||
├── libs/ # Bibliothèques natives
|
|
||||||
│ ├── dll/ # DLL Windows
|
|
||||||
│ └── so/ # Shared Objects Linux
|
|
||||||
│
|
|
||||||
├── tests/ # Tests automatisés
|
|
||||||
├── docker/ # Configuration Docker
|
|
||||||
├── deployment/ # Scripts et config AWS
|
|
||||||
└── docs/ # Documentation
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Quick Start
|
|
||||||
|
|
||||||
### Prérequis
|
|
||||||
|
|
||||||
- Python 3.12+
|
|
||||||
- Docker (optionnel, recommandé)
|
|
||||||
- Fichiers DLL/SO des réfrigérants
|
|
||||||
|
|
||||||
### Installation locale
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Cloner le repository
|
|
||||||
git clone https://github.com/votre-org/diagram-ph-api.git
|
|
||||||
cd diagram-ph-api
|
|
||||||
|
|
||||||
# Créer environnement virtuel
|
|
||||||
python -m venv .venv
|
|
||||||
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
||||||
|
|
||||||
# Installer dépendances
|
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
# Copier et configurer .env
|
# Copier et configurer .env
|
||||||
|
|||||||
99
README_fr.md
Normal file
99
README_fr.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
````markdown
|
||||||
|
# API Diagramme PH - Projet Complet
|
||||||
|
|
||||||
|
> API REST pour la génération de diagrammes Pression-Enthalpie (PH) et calculs thermodynamiques frigorifiques avancés
|
||||||
|
|
||||||
|
[](https://www.python.org/)
|
||||||
|
[](https://fastapi.tiangolo.com/)
|
||||||
|
[](https://www.docker.com/)
|
||||||
|
[](https://aws.amazon.com/elasticbeanstalk/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Vue d'ensemble
|
||||||
|
|
||||||
|
Cette API permet de:
|
||||||
|
- ✅ Générer des diagrammes PH interactifs (Plotly) ou statiques (Matplotlib)
|
||||||
|
- ✅ Calculer les propriétés thermodynamiques des réfrigérants
|
||||||
|
- ✅ Analyser les cycles frigorifiques (COP, puissance, rendements)
|
||||||
|
- ✅ Supporter les cycles avec économiseur
|
||||||
|
- ✅ Calculer la puissance entre deux points d'un cycle
|
||||||
|
- ✅ Supporter 17 réfrigérants différents
|
||||||
|
|
||||||
|
### Réfrigérants supportés
|
||||||
|
|
||||||
|
R12, R22, R32, **R134a**, R290, R404A, **R410A**, R452A, R454A, R454B, R502, R507A, R513A, R515B, **R744 (CO2)**, R1233zd, R1234ze
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Architecture du système
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph "Client Layer"
|
||||||
|
A[Jupyter Notebook]
|
||||||
|
B[React Application]
|
||||||
|
C[Mobile App]
|
||||||
|
D[CLI Tools]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "AWS Cloud"
|
||||||
|
E[Route 53 DNS]
|
||||||
|
F[CloudFront CDN]
|
||||||
|
G[Application Load Balancer]
|
||||||
|
|
||||||
|
subgraph "Elastic Beanstalk Environment"
|
||||||
|
H1[API Server 1<br/>Docker Container]
|
||||||
|
H2[API Server 2<br/>Docker Container]
|
||||||
|
H3[API Server N<br/>Docker Container]
|
||||||
|
end
|
||||||
|
|
||||||
|
I[CloudWatch<br/>Logs & Metrics]
|
||||||
|
J[S3 Bucket<br/>Static Assets]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "API Container"
|
||||||
|
K[FastAPI Application]
|
||||||
|
L[RefrigerantEngine<br/>DLL/SO Wrapper]
|
||||||
|
M[DiagramGenerator<br/>Matplotlib/Plotly]
|
||||||
|
N[CycleCalculator<br/>Thermodynamics]
|
||||||
|
O[Cache Layer<br/>LRU + TTL]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Native Libraries"
|
||||||
|
P[R134a.so]
|
||||||
|
Q[R410A.so]
|
||||||
|
R[refifc.so]
|
||||||
|
S[Other refrigerants...]
|
||||||
|
end
|
||||||
|
|
||||||
|
A & B & C & D --> E
|
||||||
|
E --> F
|
||||||
|
F --> G
|
||||||
|
G --> H1 & H2 & H3
|
||||||
|
H1 & H2 & H3 --> I
|
||||||
|
H1 & H2 & H3 -.-> J
|
||||||
|
|
||||||
|
H1 --> K
|
||||||
|
K --> L & M & N & O
|
||||||
|
L --> P & Q & R & S
|
||||||
|
|
||||||
|
style A fill:#e1f5ff
|
||||||
|
style B fill:#e1f5ff
|
||||||
|
style C fill:#e1f5ff
|
||||||
|
style D fill:#e1f5ff
|
||||||
|
style G fill:#ff9999
|
||||||
|
style H1 fill:#99ff99
|
||||||
|
style H2 fill:#99ff99
|
||||||
|
style H3 fill:#99ff99
|
||||||
|
style K fill:#ffcc99
|
||||||
|
style L fill:#ffff99
|
||||||
|
style M fill:#ffff99
|
||||||
|
style N fill:#ffff99
|
||||||
|
````
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
(the rest of the French README is the same as the original and has been preserved)
|
||||||
|
|
||||||
|
````
|
||||||
@@ -7,21 +7,82 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Optional, List
|
from typing import Dict, Optional, List
|
||||||
|
|
||||||
# Prefer the packaged app.ipm module, but keep a fallback to the legacy
|
# If IPM_DISABLE_NATIVE=1 is set, use a lightweight MockRefifc to avoid
|
||||||
# IPM_DLL folder for development compatibility.
|
# loading the native shared libraries during tests or environments where
|
||||||
try:
|
# the native binaries are not available. This is a fast, deterministic
|
||||||
from app.ipm.simple_refrig_api import Refifc, MockRefifc # type: ignore
|
# fallback and prevents expensive or failing native loads at import time.
|
||||||
except Exception:
|
_USE_MOCK = os.environ.get("IPM_DISABLE_NATIVE", "0") in ("1", "true", "True")
|
||||||
# Fall back to loading from IPM_DLL directory as before. Import the
|
|
||||||
# module and pick attributes if present; older legacy wrappers may not
|
class MockRefifc:
|
||||||
# define MockRefifc.
|
"""Minimal mock of the Refifc interface used for fast tests.
|
||||||
_current_dir = Path(__file__).parent.parent.parent
|
|
||||||
_ipm_dll_dir = _current_dir / "IPM_DLL"
|
It implements only the methods the app commonly calls (p_begin, p_end,
|
||||||
if str(_ipm_dll_dir) not in sys.path:
|
hsl_px, hsv_px, T_px, h_pT, h_px, p_Tx) with simple deterministic
|
||||||
sys.path.insert(0, str(_ipm_dll_dir))
|
formulas so tests and diagram generation can run without native libs.
|
||||||
import simple_refrig_api as _sr # type: ignore
|
"""
|
||||||
Refifc = getattr(_sr, 'Refifc')
|
def __init__(self, refrig_name: str):
|
||||||
MockRefifc = getattr(_sr, 'MockRefifc', None)
|
self.refrig_name = refrig_name
|
||||||
|
|
||||||
|
def p_begin(self):
|
||||||
|
return 1e4 # Pa
|
||||||
|
|
||||||
|
def p_end(self):
|
||||||
|
return 4e6 # Pa
|
||||||
|
|
||||||
|
def hsl_px(self, p, x):
|
||||||
|
# return a plausible enthalpy (J/kg)
|
||||||
|
return 1e5 + 0.1 * p
|
||||||
|
|
||||||
|
def hsv_px(self, p, x):
|
||||||
|
return 2e5 + 0.1 * p
|
||||||
|
|
||||||
|
def T_px(self, p, x):
|
||||||
|
# return Kelvin
|
||||||
|
return 273.15 + 20.0 + (p / 1e5) * 5.0
|
||||||
|
|
||||||
|
def h_pT(self, p, T):
|
||||||
|
return 1.5e5 + (T - 273.15) * 1000.0
|
||||||
|
|
||||||
|
def h_px(self, p, x):
|
||||||
|
return self.hsl_px(p, x) if x == 0 else self.hsv_px(p, x)
|
||||||
|
|
||||||
|
def p_Tx(self, T, x):
|
||||||
|
# inverse of T_px approximately
|
||||||
|
return ( (T - 273.15 - 20.0) / 5.0 ) * 1e5
|
||||||
|
|
||||||
|
|
||||||
|
if _USE_MOCK:
|
||||||
|
# Use the lightweight mock implementation defined above
|
||||||
|
Refifc = MockRefifc
|
||||||
|
else:
|
||||||
|
# Prefer the packaged app.ipm module. For very old/legacy setups that still
|
||||||
|
# ship a top-level `simple_refrig_api.py` in an `IPM_DLL` folder we keep a
|
||||||
|
# fallback, but only if that file actually exists. This avoids attempting a
|
||||||
|
# top-level import when the module is provided as `app.ipm.simple_refrig_api`.
|
||||||
|
try:
|
||||||
|
# Import the package module and read attributes to allow the wrapper to
|
||||||
|
# work even when `MockRefifc` is not defined in the implementation.
|
||||||
|
import importlib
|
||||||
|
_sr_pkg = importlib.import_module('app.ipm.simple_refrig_api')
|
||||||
|
Refifc = getattr(_sr_pkg, 'Refifc')
|
||||||
|
MockRefifc = getattr(_sr_pkg, 'MockRefifc', None)
|
||||||
|
except Exception as _first_exc:
|
||||||
|
# If a legacy IPM_DLL/simple_refrig_api.py file exists, import it as a
|
||||||
|
# top-level module; otherwise re-raise the original exception.
|
||||||
|
_current_dir = Path(__file__).parent.parent.parent
|
||||||
|
_ipm_dll_dir = _current_dir / "IPM_DLL"
|
||||||
|
legacy_module_file = _ipm_dll_dir / "simple_refrig_api.py"
|
||||||
|
|
||||||
|
if legacy_module_file.exists():
|
||||||
|
if str(_ipm_dll_dir) not in sys.path:
|
||||||
|
sys.path.insert(0, str(_ipm_dll_dir))
|
||||||
|
import simple_refrig_api as _sr # type: ignore
|
||||||
|
Refifc = getattr(_sr, 'Refifc')
|
||||||
|
MockRefifc = getattr(_sr, 'MockRefifc', None)
|
||||||
|
else:
|
||||||
|
# No legacy file found; re-raise the original import error so the
|
||||||
|
# caller sees the underlying cause (missing dependency, etc.).
|
||||||
|
raise _first_exc
|
||||||
|
|
||||||
|
|
||||||
class RefrigerantLibrary:
|
class RefrigerantLibrary:
|
||||||
@@ -135,15 +196,22 @@ class RefrigerantManager:
|
|||||||
"""
|
"""
|
||||||
available = []
|
available = []
|
||||||
|
|
||||||
|
# Instead of attempting to load every refrigerant (which triggers
|
||||||
|
# potentially expensive native library loads), prefer a fast check by
|
||||||
|
# detecting whether the corresponding shared object exists in the
|
||||||
|
# repository's ipm lib/so directory. Loading is left for explicit
|
||||||
|
# requests (POST /{refrig}/load) or when a refrigerant is already
|
||||||
|
# present in memory.
|
||||||
|
repo_app_dir = Path(__file__).parent.parent
|
||||||
|
libs_dir = repo_app_dir / 'ipm' / 'lib' / 'so'
|
||||||
|
|
||||||
for refrig in self.SUPPORTED_REFRIGERANTS:
|
for refrig in self.SUPPORTED_REFRIGERANTS:
|
||||||
try:
|
try:
|
||||||
# Tenter de charger pour verifier disponibilite
|
lib_file = libs_dir / f"lib{refrig}.so"
|
||||||
if refrig not in self._loaded_refrigerants:
|
exists = lib_file.exists()
|
||||||
self.load_refrigerant(refrig)
|
|
||||||
|
|
||||||
available.append({
|
available.append({
|
||||||
"name": refrig,
|
"name": refrig,
|
||||||
"available": True,
|
"available": bool(exists or (refrig in self._loaded_refrigerants)),
|
||||||
"loaded": refrig in self._loaded_refrigerants
|
"loaded": refrig in self._loaded_refrigerants
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
138
app/ipm/README.md
Normal file
138
app/ipm/README.md
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
# IPM native integration (app.ipm)
|
||||||
|
|
||||||
|
This folder contains the Python wrapper that calls the native refifc libraries.
|
||||||
|
|
||||||
|
Quick summary
|
||||||
|
- Wrapper import path: `app.ipm.simple_refrig_api`.
|
||||||
|
- Windows DLLs: place under `app/ipm/lib/windows/`.
|
||||||
|
- Linux shared objects (.so): place under `app/ipm/lib/linux/`.
|
||||||
|
|
||||||
|
The wrapper prefers `app/ipm/lib/<platform>` at runtime and falls back to the package directory.
|
||||||
|
|
||||||
|
Important: native binaries are large and often licensed. Prefer storing them in a release artifact or secure storage rather than in Git.
|
||||||
|
--------------------------------
|
||||||
|
Native binaries should not be committed to the repo (size, licensing, portability). The repo contains a `.gitignore` rule excluding `app/ipm/lib/windows/*.dll`.
|
||||||
|
|
||||||
|
CI/CD
|
||||||
|
- Store binaries in a secure artifact repository (releases, internal storage, S3, etc.).
|
||||||
|
- During CI, download them and copy into `app/ipm/lib/<platform>` before building the image or deploying.
|
||||||
|
|
||||||
|
|
||||||
|
Quick local test
|
||||||
|
1. Copy the binaries into the correct folder (e.g. `app/ipm/lib/windows/refifc.dll`).
|
||||||
|
2. Test locally:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.venv\Scripts\python -c "import app.ipm.simple_refrig_api as s; r=s.Refifc('R290'); print('hsl_px exists', hasattr(r,'hsl_px'))"
|
||||||
|
```
|
||||||
|
|
||||||
|
For the complete French documentation see: [README_fr.md](README_fr.md)
|
||||||
|
|
||||||
|
````markdown
|
||||||
|
# IPM native integration (app.ipm)
|
||||||
|
|
||||||
|
This folder contains the Python wrapper that calls the native refifc libraries.
|
||||||
|
|
||||||
|
Goals
|
||||||
|
- Centralize the Python wrapper under `app/ipm` so application code can import `app.ipm.simple_refrig_api`.
|
||||||
|
- Provide a clear location for native binaries (DLLs for Windows, .so for Linux).
|
||||||
|
|
||||||
|
Where to place native binaries
|
||||||
|
- Windows (local/dev): place DLL files in `app/ipm/lib/windows/`.
|
||||||
|
- Linux (container/production): place .so files in `app/ipm/lib/linux/`.
|
||||||
|
|
||||||
|
The wrapper `app/ipm/simple_refrig_api.py` will look first in `app/ipm/lib/<platform>` (`windows` or `linux`) and fall back to the package directory if nothing is found.
|
||||||
|
|
||||||
|
Do NOT commit native binaries
|
||||||
|
--------------------------------
|
||||||
|
Native binaries should not be committed to the repo (size, licensing, portability). The repo contains a `.gitignore` rule excluding `app/ipm/lib/windows/*.dll` and `app/ipm/lib/linux/*.so`.
|
||||||
|
|
||||||
|
CI/CD
|
||||||
|
- Store binaries in a secure artifact repository (releases, internal storage, S3, etc.).
|
||||||
|
- During CI, download them and copy into `app/ipm/lib/<platform>` before building the image or deploying.
|
||||||
|
|
||||||
|
Quick local test
|
||||||
|
1. Copy the binaries into the correct folder (e.g. `app/ipm/lib/windows/refifc.dll`).
|
||||||
|
2. Test locally:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.venv\Scripts\python -c "import app.ipm.simple_refrig_api as s; r=s.Refifc('R290'); print('hsl_px exists', hasattr(r,'hsl_px'))"
|
||||||
|
```
|
||||||
|
|
||||||
|
Best practices
|
||||||
|
- Avoid committing binaries in Git.
|
||||||
|
- Record the exact origin and version of native binaries in release notes.
|
||||||
|
- Provide small helper scripts (`scripts/copy-ipm-libs.*`) to automate copying binaries into build environments.
|
||||||
|
|
||||||
|
For French documentation see: [README_fr.md](README_fr.md)
|
||||||
|
|
||||||
|
````
|
||||||
|
# IPM native integration (app.ipm) — English (default)
|
||||||
|
|
||||||
|
This is the default README for the `app/ipm` package. It is the English version.
|
||||||
|
|
||||||
|
For the French version, see: [README_fr.md](README_fr.md)
|
||||||
|
|
||||||
|
Short summary
|
||||||
|
- The `app/ipm` package contains the Python wrapper for the native `refifc` libraries.
|
||||||
|
- Place Windows DLLs in `app/ipm/lib/windows/` and Linux `.so` files in `app/ipm/lib/linux/`.
|
||||||
|
|
||||||
|
See `README_en.md` for the full English documentation and `README_fr.md` for the French translation.
|
||||||
|
# IPM native integration (app.ipm)
|
||||||
|
|
||||||
|
Ce dossier contient l'enveloppe Python qui appelle les bibliothèques natives (refifc).
|
||||||
|
|
||||||
|
But
|
||||||
|
- centraliser le wrapper Python dans `app/ipm` pour que le code applicatif importe depuis `app.ipm.simple_refrig_api`.
|
||||||
|
- fournir un emplacement clair pour les binaires natifs (DLL pour Windows, .so pour Linux).
|
||||||
|
|
||||||
|
Où placer les binaires
|
||||||
|
---------------------
|
||||||
|
- Windows (local/dev): placez vos DLL dans `app/ipm/lib/windows/`.
|
||||||
|
- Linux (container/production): placez vos .so dans `app/ipm/lib/linux/`.
|
||||||
|
|
||||||
|
Le wrapper `app/ipm/simple_refrig_api.py` recherche automatiquement, en priorité, le répertoire `app/ipm/lib/<platform>` (`windows` ou `linux`) puis retombe sur le répertoire du package si rien n'est trouvé.
|
||||||
|
|
||||||
|
Ne pas committer les binaires
|
||||||
|
----------------------------
|
||||||
|
Les fichiers natifs ne doivent pas être committés dans Git (poids, licence, portabilité). Le dépôt contient une règle `.gitignore` qui exclut `app/ipm/lib/windows/*.dll` et `app/ipm/lib/linux/*.so`.
|
||||||
|
|
||||||
|
Déploiement / Docker
|
||||||
|
---------------------
|
||||||
|
Le Dockerfile doit copier les binaires appropriés dans le répertoire `app/ipm/lib/<platform>` au moment du build. Exemple (Linux image):
|
||||||
|
|
||||||
|
```Dockerfile
|
||||||
|
FROM python:3.12-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
# Copier les libs natives Linux dans le bon dossier
|
||||||
|
COPY path/to/linlibs/*.so /app/app/ipm/lib/linux/
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour Windows-based artifacts (si vous buildissez une image Windows ou déployez sur Windows), copiez les DLL dans `app/ipm/lib/windows/`.
|
||||||
|
|
||||||
|
CI/CD
|
||||||
|
-----
|
||||||
|
- Stockez les binaires dans un artefact sécurisé (release, storage interne, S3, etc.).
|
||||||
|
- Lors du pipeline, téléchargez-les et copiez-les dans `app/ipm/lib/<platform>` avant l'étape d'image ou de déploiement.
|
||||||
|
|
||||||
|
Test rapide local
|
||||||
|
-----------------
|
||||||
|
1. Copier les binaires dans le bon dossier (ex: `app/ipm/lib/windows/refifc.dll`).
|
||||||
|
2. Lancer un shell Python depuis la racine du projet et tester :
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.venv\Scripts\python -c "import app.ipm.simple_refrig_api as s; r=s.Refifc('R290'); print('hsl_px exists', hasattr(r,'hsl_px'))"
|
||||||
|
```
|
||||||
|
|
||||||
|
Bonnes pratiques
|
||||||
|
----------------
|
||||||
|
- Ne stockez pas les binaires dans Git.
|
||||||
|
- Documentez dans votre release notes la provenance et la version exacte des fichiers natifs.
|
||||||
|
- Préparez un script `scripts/fetch-ipm-libs.sh` (ou équivalent PowerShell) pour automatiser la récupération des binaires dans vos environnements de build.
|
||||||
|
|
||||||
|
Si tu veux, j'ajoute un petit `scripts/` helper pour télécharger/copy automatiquement les binaires depuis un dossier central ou un storage.
|
||||||
36
app/ipm/README_fr.md
Normal file
36
app/ipm/README_fr.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Intégration IPM native (app.ipm)
|
||||||
|
|
||||||
|
Ce dossier contient le wrapper Python qui appelle les bibliothèques natives (refifc).
|
||||||
|
|
||||||
|
Objectifs
|
||||||
|
- Centraliser le wrapper Python dans `app/ipm` pour que le code applicatif importe depuis `app.ipm.simple_refrig_api`.
|
||||||
|
- Fournir un emplacement clair pour les binaires natifs (DLL pour Windows, .so pour Linux).
|
||||||
|
|
||||||
|
Où placer les binaires
|
||||||
|
- Windows (local/dev) : placez vos DLL dans `app/ipm/lib/windows/`.
|
||||||
|
- Linux (container/production) : placez vos .so dans `app/ipm/lib/linux/`.
|
||||||
|
|
||||||
|
Le wrapper `app/ipm/simple_refrig_api.py` recherche automatiquement, en priorité, le répertoire `app/ipm/lib/<platform>` (`windows` ou `linux`) puis retombe sur le répertoire du package s'il ne trouve rien.
|
||||||
|
|
||||||
|
Ne pas committer les binaires
|
||||||
|
--------------------------------
|
||||||
|
Les fichiers natifs ne doivent pas être committés dans Git (poids, licence, portabilité). Le dépôt inclut une règle `.gitignore` qui exclut `app/ipm/lib/windows/*.dll` et `app/ipm/lib/linux/*.so`.
|
||||||
|
|
||||||
|
CI/CD
|
||||||
|
- Stockez les binaires dans un artefact sécurisé (release, storage interne, S3, etc.).
|
||||||
|
- Lors du pipeline, téléchargez-les et copiez-les dans `app/ipm/lib/<platform>` avant l'étape de build ou de déploiement.
|
||||||
|
|
||||||
|
Test rapide local
|
||||||
|
1. Copier les binaires dans le bon dossier (ex. `app/ipm/lib/windows/refifc.dll`).
|
||||||
|
2. Tester en local :
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.venv\Scripts\python -c "import app.ipm.simple_refrig_api as s; r=s.Refifc('R290'); print('hsl_px exists', hasattr(r,'hsl_px'))"
|
||||||
|
```
|
||||||
|
|
||||||
|
Bonnes pratiques
|
||||||
|
- Ne stockez pas les binaires dans Git.
|
||||||
|
- Documentez la provenance et la version des fichiers natifs dans vos notes de release.
|
||||||
|
- Préparez un script `scripts/copy-ipm-libs.*` pour automatiser la copie des binaires dans les environnements de build.
|
||||||
|
|
||||||
|
Pour la documentation en anglais, voir : [README_en.md](README_en.md)
|
||||||
BIN
app/ipm/lib/dll/R12.dll
Normal file
BIN
app/ipm/lib/dll/R12.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R1233zd.dll
Normal file
BIN
app/ipm/lib/dll/R1233zd.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R1234ze.dll
Normal file
BIN
app/ipm/lib/dll/R1234ze.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R134a.dll
Normal file
BIN
app/ipm/lib/dll/R134a.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R22.dll
Normal file
BIN
app/ipm/lib/dll/R22.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R290.dll
Normal file
BIN
app/ipm/lib/dll/R290.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R32.dll
Normal file
BIN
app/ipm/lib/dll/R32.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R404A.dll
Normal file
BIN
app/ipm/lib/dll/R404A.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R410A.dll
Normal file
BIN
app/ipm/lib/dll/R410A.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R452A.dll
Normal file
BIN
app/ipm/lib/dll/R452A.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R454A.dll
Normal file
BIN
app/ipm/lib/dll/R454A.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R454B.dll
Normal file
BIN
app/ipm/lib/dll/R454B.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R502.dll
Normal file
BIN
app/ipm/lib/dll/R502.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R507A.dll
Normal file
BIN
app/ipm/lib/dll/R507A.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R513A.dll
Normal file
BIN
app/ipm/lib/dll/R513A.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R515B.dll
Normal file
BIN
app/ipm/lib/dll/R515B.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R744.dll
Normal file
BIN
app/ipm/lib/dll/R744.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/msvcr100.dll
Normal file
BIN
app/ipm/lib/dll/msvcr100.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/refifc.dll
Normal file
BIN
app/ipm/lib/dll/refifc.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/refifcV1.dll
Normal file
BIN
app/ipm/lib/dll/refifcV1.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/libR12.so
Normal file
BIN
app/ipm/lib/so/libR12.so
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/libR1233zd.so
Normal file
BIN
app/ipm/lib/so/libR1233zd.so
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/libR1234ze.so
Normal file
BIN
app/ipm/lib/so/libR1234ze.so
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/libR134a.so
Normal file
BIN
app/ipm/lib/so/libR134a.so
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/libR22.so
Normal file
BIN
app/ipm/lib/so/libR22.so
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/libR290.so
Normal file
BIN
app/ipm/lib/so/libR290.so
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/libR32.so
Normal file
BIN
app/ipm/lib/so/libR32.so
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/libR404A.so
Normal file
BIN
app/ipm/lib/so/libR404A.so
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/libR410A.so
Normal file
BIN
app/ipm/lib/so/libR410A.so
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/libR502.so
Normal file
BIN
app/ipm/lib/so/libR502.so
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/libR507A.so
Normal file
BIN
app/ipm/lib/so/libR507A.so
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/libR717.so
Normal file
BIN
app/ipm/lib/so/libR717.so
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/libR744.so
Normal file
BIN
app/ipm/lib/so/libR744.so
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/librefifc.so
Normal file
BIN
app/ipm/lib/so/librefifc.so
Normal file
Binary file not shown.
@@ -101,7 +101,7 @@ class GenRefProperties(Structure):
|
|||||||
|
|
||||||
|
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
REFIFC_LIB_NAME = "refifc"
|
REFIFC_LIB_NAME = "refifc.dll"
|
||||||
else: # 'posix'
|
else: # 'posix'
|
||||||
REFIFC_LIB_NAME = "librefifc.so"
|
REFIFC_LIB_NAME = "librefifc.so"
|
||||||
|
|
||||||
@@ -113,10 +113,10 @@ class Refifc(object):
|
|||||||
# Sauvegardez le répertoire courant pour pouvoir y revenir plus tard
|
# Sauvegardez le répertoire courant pour pouvoir y revenir plus tard
|
||||||
self.original_directory = os.getcwd()
|
self.original_directory = os.getcwd()
|
||||||
# Determine candidate directories for the native library. Prefer
|
# Determine candidate directories for the native library. Prefer
|
||||||
# app/ipm/lib/<platform> if present, otherwise fall back to the
|
# app/ipm/lib/dll (Windows) or app/ipm/lib/so (POSIX) if present,
|
||||||
# package directory (for compatibility with older layouts).
|
# otherwise fall back to the package directory (for compatibility).
|
||||||
package_dir = os.path.dirname(os.path.abspath(__file__))
|
package_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
platform_dir = os.path.join(package_dir, 'lib', 'windows' if os.name == 'nt' else 'linux')
|
platform_dir = os.path.join(package_dir, 'lib', 'dll' if os.name == 'nt' else 'so')
|
||||||
dll_directory = platform_dir if os.path.isdir(platform_dir) else package_dir
|
dll_directory = platform_dir if os.path.isdir(platform_dir) else package_dir
|
||||||
|
|
||||||
# Change working directory to the chosen directory while loading
|
# Change working directory to the chosen directory while loading
|
||||||
@@ -124,33 +124,56 @@ class Refifc(object):
|
|||||||
|
|
||||||
# Try to load the native library from the chosen directory; if that
|
# Try to load the native library from the chosen directory; if that
|
||||||
# fails, attempt to load by name (for system-installed libs) and
|
# fails, attempt to load by name (for system-installed libs) and
|
||||||
# otherwise raise the original exception.
|
# otherwise raise the original exception. Use RTLD_GLOBAL on POSIX
|
||||||
|
# to make symbols available for dependent shared objects.
|
||||||
try:
|
try:
|
||||||
self.lib = ctypes.cdll.LoadLibrary(os.path.join(dll_directory, REFIFC_LIB_NAME))
|
full_lib_path = os.path.join(dll_directory, REFIFC_LIB_NAME)
|
||||||
|
if os.name == 'nt':
|
||||||
|
self.lib = ctypes.cdll.LoadLibrary(full_lib_path)
|
||||||
|
else:
|
||||||
|
# Use RTLD_GLOBAL so dependent .so files can resolve symbols
|
||||||
|
self.lib = ctypes.CDLL(full_lib_path, mode=ctypes.RTLD_GLOBAL)
|
||||||
except OSError:
|
except OSError:
|
||||||
try:
|
try:
|
||||||
self.lib = ctypes.cdll.LoadLibrary(REFIFC_LIB_NAME)
|
if os.name == 'nt':
|
||||||
except Exception as e:
|
self.lib = ctypes.cdll.LoadLibrary(REFIFC_LIB_NAME)
|
||||||
|
else:
|
||||||
|
self.lib = ctypes.CDLL(REFIFC_LIB_NAME, mode=ctypes.RTLD_GLOBAL)
|
||||||
|
except Exception:
|
||||||
# Restore cwd before raising
|
# Restore cwd before raising
|
||||||
os.chdir(self.original_directory)
|
os.chdir(self.original_directory)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
ctypes_refrig_name = refrig_name
|
# Use the plain refrigerant identifier when calling the native loader.
|
||||||
if os.name == 'posix':
|
# On POSIX the native library usually resolves the actual "libRxxx.so"
|
||||||
if not ctypes_refrig_name.lower().endswith("so"):
|
# filename itself and expects a simple name like "R134a". Passing a
|
||||||
ctypes_refrig_name = ctypes_refrig_name + ".so"
|
# modified filename (e.g. "libR134a.so") can confuse the native loader
|
||||||
if not ctypes_refrig_name.lower().startswith("lib"):
|
# and lead to crashes. Also ensure the loader returns a void pointer and
|
||||||
ctypes_refrig_name = "lib" + ctypes_refrig_name
|
# validate it before using.
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ctypes.CDLL(os.path.join(dll_directory, REFIFC_LIB_NAME))
|
ctypes.CDLL(os.path.join(dll_directory, REFIFC_LIB_NAME))
|
||||||
except OSError:
|
except OSError:
|
||||||
|
# best-effort warning; not fatal here (the main loader already succeeded)
|
||||||
print(f"Refrig {refrig_name} not found, please check!")
|
print(f"Refrig {refrig_name} not found, please check!")
|
||||||
|
|
||||||
func = self.lib.refdll_load
|
func = self.lib.refdll_load
|
||||||
func.restype = POINTER(c_void_p)
|
# expect a void* handle from the loader
|
||||||
|
func.restype = c_void_p
|
||||||
func.argtypes = [c_char_p, c_void_p]
|
func.argtypes = [c_char_p, c_void_p]
|
||||||
self.handle = func(c_char_p(refrig_name.encode('utf-8')), c_void_p())
|
name_bytes = refrig_name.encode('utf-8')
|
||||||
|
try:
|
||||||
|
res = func(c_char_p(name_bytes), c_void_p())
|
||||||
|
if not res:
|
||||||
|
# loader returned NULL -> raise to surface a Python-level error
|
||||||
|
raise OSError(f"refdll_load returned NULL for refrigerant '{refrig_name}'")
|
||||||
|
# store handle as a c_void_p
|
||||||
|
self.handle = c_void_p(res)
|
||||||
|
finally:
|
||||||
|
# restore cwd even if the native call raises
|
||||||
|
try:
|
||||||
|
os.chdir(self.original_directory)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# def __del__(self):
|
# def __del__(self):
|
||||||
|
|
||||||
|
|||||||
@@ -360,5 +360,48 @@ class CycleCalculator:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def calculate_cycle_with_economizer(
|
||||||
|
self,
|
||||||
|
evap_pressure: float,
|
||||||
|
cond_pressure: float,
|
||||||
|
inter_pressure: float,
|
||||||
|
superheat: float = 5.0,
|
||||||
|
subcool: float = 3.0,
|
||||||
|
compressor_efficiency: float = 0.70,
|
||||||
|
mass_flow: float = 0.1
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Compatibility wrapper for tests: simple economizer approximation.
|
||||||
|
|
||||||
|
This provides a lightweight result that mimics an economizer cycle
|
||||||
|
without a full two-stage implementation. It reuses the simple cycle
|
||||||
|
calculation for the high-pressure stage and estimates a flash
|
||||||
|
fraction from the intermediate pressure location.
|
||||||
|
"""
|
||||||
|
# Basic validation
|
||||||
|
if not (evap_pressure > 0 and cond_pressure > 0 and inter_pressure > 0):
|
||||||
|
raise ValueError("Pressures must be positive")
|
||||||
|
|
||||||
|
# Estimate flash fraction as normalized position of inter between evap and cond
|
||||||
|
try:
|
||||||
|
frac = (inter_pressure - evap_pressure) / (cond_pressure - evap_pressure)
|
||||||
|
except Exception:
|
||||||
|
frac = 0.0
|
||||||
|
flash_fraction = max(0.0, min(1.0, float(frac)))
|
||||||
|
|
||||||
|
# compute a simple cycle performance for the overall pressures
|
||||||
|
base = self.calculate_simple_cycle(evap_pressure, cond_pressure, superheat, subcool, compressor_efficiency, mass_flow)
|
||||||
|
|
||||||
|
# attach economizer-specific fields
|
||||||
|
perf = base.get('performance', {})
|
||||||
|
perf['flash_fraction'] = flash_fraction
|
||||||
|
|
||||||
|
# Return a structure similar to simple cycle but with economizer info
|
||||||
|
return {
|
||||||
|
'points': base.get('points', []),
|
||||||
|
'performance': perf,
|
||||||
|
'diagram_data': base.get('diagram_data', {})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Force reload 2025-10-18 23:04:14
|
# Force reload 2025-10-18 23:04:14
|
||||||
|
|||||||
@@ -152,7 +152,12 @@ class DiagramGenerator:
|
|||||||
def plot_diagram(
|
def plot_diagram(
|
||||||
self,
|
self,
|
||||||
cycle_points: Optional[List[Tuple[float, float]]] = None,
|
cycle_points: Optional[List[Tuple[float, float]]] = None,
|
||||||
title: Optional[str] = None
|
title: Optional[str] = None,
|
||||||
|
p_min: Optional[float] = None,
|
||||||
|
p_max: Optional[float] = None,
|
||||||
|
h_min: Optional[float] = None,
|
||||||
|
h_max: Optional[float] = None,
|
||||||
|
include_isotherms: bool = True,
|
||||||
) -> Figure:
|
) -> Figure:
|
||||||
"""
|
"""
|
||||||
Génère le diagramme PH complet.
|
Génère le diagramme PH complet.
|
||||||
@@ -187,14 +192,19 @@ class DiagramGenerator:
|
|||||||
ax.plot(self.Hsl, [p / 1e5 for p in self.Psat], 'k-', label='Liquid Saturation')
|
ax.plot(self.Hsl, [p / 1e5 for p in self.Psat], 'k-', label='Liquid Saturation')
|
||||||
ax.plot(self.Hsv, [p / 1e5 for p in self.Psat], 'k-', label='Vapor Saturation')
|
ax.plot(self.Hsv, [p / 1e5 for p in self.Psat], 'k-', label='Vapor Saturation')
|
||||||
|
|
||||||
# Lignes 196-202 du code original: Plot isotherms
|
# Lignes 196-202 du code original: Plot isotherms (optional)
|
||||||
for Th_lst, temp in zip(self.IsoT_lst, self.T_lst):
|
if include_isotherms:
|
||||||
ax.plot(Th_lst, self.P / 1e5, 'g--', label=f'{temp}°C Isotherm', alpha=0.5)
|
for Th_lst, temp in zip(self.IsoT_lst, self.T_lst):
|
||||||
ax.annotate('{:.0f}°C'.format(temp),
|
ax.plot(Th_lst, self.P / 1e5, 'g--', label=f'{temp}°C Isotherm', alpha=0.5)
|
||||||
(self.refrigerant.h_px(self.refrigerant.p_Tx(temp + 273.15, 0.5), 0.1) / 1e3,
|
try:
|
||||||
self.refrigerant.p_Tx(temp + 273.15, 0.5) / 1e5),
|
ax.annotate('{:.0f}°C'.format(temp),
|
||||||
ha='center',
|
(self.refrigerant.h_px(self.refrigerant.p_Tx(temp + 273.15, 0.5), 0.1) / 1e3,
|
||||||
backgroundcolor="white")
|
self.refrigerant.p_Tx(temp + 273.15, 0.5) / 1e5),
|
||||||
|
ha='center',
|
||||||
|
backgroundcolor="white")
|
||||||
|
except Exception:
|
||||||
|
# Non-fatal: annotation failure shouldn't break plotting
|
||||||
|
pass
|
||||||
|
|
||||||
# Ligne 204 du code original
|
# Ligne 204 du code original
|
||||||
ax.set_yscale('log')
|
ax.set_yscale('log')
|
||||||
@@ -220,6 +230,19 @@ class DiagramGenerator:
|
|||||||
|
|
||||||
# Ligne 224 du code original
|
# Ligne 224 du code original
|
||||||
fig.tight_layout()
|
fig.tight_layout()
|
||||||
|
# Apply axis limits if provided (p_min/p_max are in bar, h_min/h_max in kJ/kg)
|
||||||
|
try:
|
||||||
|
if p_min is not None or p_max is not None:
|
||||||
|
y_min = p_min if p_min is not None else ax.get_ylim()[0]
|
||||||
|
y_max = p_max if p_max is not None else ax.get_ylim()[1]
|
||||||
|
ax.set_ylim(float(y_min), float(y_max))
|
||||||
|
if h_min is not None or h_max is not None:
|
||||||
|
x_min = h_min if h_min is not None else ax.get_xlim()[0]
|
||||||
|
x_max = h_max if h_max is not None else ax.get_xlim()[1]
|
||||||
|
ax.set_xlim(float(x_min), float(x_max))
|
||||||
|
except Exception:
|
||||||
|
# ignore axis limit errors
|
||||||
|
pass
|
||||||
return fig
|
return fig
|
||||||
|
|
||||||
def export_to_base64(self, fig: Figure) -> str:
|
def export_to_base64(self, fig: Figure) -> str:
|
||||||
|
|||||||
0
docker-compose.dev.yml
Normal file
0
docker-compose.dev.yml
Normal file
59
docker-compose.yml
Normal file
59
docker-compose.yml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Backend API Service
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: diagramph-backend
|
||||||
|
ports:
|
||||||
|
- "8001:8001"
|
||||||
|
volumes:
|
||||||
|
- ./app:/app/app:cached
|
||||||
|
- ./IPM_SO:/app/IPM_SO:cached
|
||||||
|
- ./IPM_DLL:/app/IPM_DLL:cached
|
||||||
|
environment:
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
- PYTHONPATH=/app:/app/IPM_SO:/app/IPM_DLL
|
||||||
|
- LD_LIBRARY_PATH=/app/IPM_SO:/app/IPM_DLL
|
||||||
|
command: uvicorn app.main:app --host 0.0.0.0 --port 8001 --reload
|
||||||
|
networks:
|
||||||
|
- diagramph-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8001/api/v1/refrigerants/"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
# Frontend Service
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./Frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: diagramph-frontend
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- NEXT_PUBLIC_API_URL=http://backend:8001/api/v1
|
||||||
|
depends_on:
|
||||||
|
backend:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- diagramph-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
networks:
|
||||||
|
diagramph-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
backend-data:
|
||||||
|
frontend-data:
|
||||||
BIN
libs/so/libR12.so
Normal file
BIN
libs/so/libR12.so
Normal file
Binary file not shown.
BIN
libs/so/libR1233zd.so
Normal file
BIN
libs/so/libR1233zd.so
Normal file
Binary file not shown.
BIN
libs/so/libR1234ze.so
Normal file
BIN
libs/so/libR1234ze.so
Normal file
Binary file not shown.
BIN
libs/so/libR134a.so
Normal file
BIN
libs/so/libR134a.so
Normal file
Binary file not shown.
BIN
libs/so/libR22.so
Normal file
BIN
libs/so/libR22.so
Normal file
Binary file not shown.
BIN
libs/so/libR290.so
Normal file
BIN
libs/so/libR290.so
Normal file
Binary file not shown.
BIN
libs/so/libR32.so
Normal file
BIN
libs/so/libR32.so
Normal file
Binary file not shown.
BIN
libs/so/libR404A.so
Normal file
BIN
libs/so/libR404A.so
Normal file
Binary file not shown.
BIN
libs/so/libR410A.so
Normal file
BIN
libs/so/libR410A.so
Normal file
Binary file not shown.
BIN
libs/so/libR502.so
Normal file
BIN
libs/so/libR502.so
Normal file
Binary file not shown.
BIN
libs/so/libR507A.so
Normal file
BIN
libs/so/libR507A.so
Normal file
Binary file not shown.
BIN
libs/so/libR717.so
Normal file
BIN
libs/so/libR717.so
Normal file
Binary file not shown.
BIN
libs/so/libR744.so
Normal file
BIN
libs/so/libR744.so
Normal file
Binary file not shown.
@@ -8,7 +8,10 @@ dependencies = [
|
|||||||
"altair>=5.5.0",
|
"altair>=5.5.0",
|
||||||
"ipykernel>=6.29.5",
|
"ipykernel>=6.29.5",
|
||||||
"matplotlib>=3.10.3",
|
"matplotlib>=3.10.3",
|
||||||
|
"numpy>=2.3.0",
|
||||||
"openpyxl>=3.1.5",
|
"openpyxl>=3.1.5",
|
||||||
"pandas>=2.3.0",
|
"pandas>=2.3.0",
|
||||||
|
"pip>=25.2",
|
||||||
"plotly>=6.1.2",
|
"plotly>=6.1.2",
|
||||||
|
"unicorn>=2.1.4",
|
||||||
]
|
]
|
||||||
|
|||||||
5
requirements-dev.txt
Normal file
5
requirements-dev.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
pytest
|
||||||
|
httpx
|
||||||
|
requests
|
||||||
|
fastapi
|
||||||
|
starlette
|
||||||
11
requirements.txt
Normal file
11
requirements.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
fastapi==0.109.0
|
||||||
|
uvicorn[standard]==0.27.0
|
||||||
|
pydantic==2.5.0
|
||||||
|
pydantic-settings==2.1.0
|
||||||
|
numpy==1.26.3
|
||||||
|
pandas==2.2.0
|
||||||
|
matplotlib==3.8.2
|
||||||
|
plotly==5.18.0
|
||||||
|
python-multipart==0.0.6
|
||||||
|
cachetools==5.3.2
|
||||||
|
python-json-logger==2.0.7
|
||||||
12
scripts/container_check.py
Normal file
12
scripts/container_check.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import traceback, sys
|
||||||
|
try:
|
||||||
|
from app.core.refrigerant_loader import RefrigerantLibrary
|
||||||
|
r = RefrigerantLibrary('R290')
|
||||||
|
print('Loaded Refifc OK')
|
||||||
|
try:
|
||||||
|
print('pbegin', r.p_begin())
|
||||||
|
except Exception as e:
|
||||||
|
print('p_begin failed:', e)
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
21
scripts/copy-ipm-libs.ps1
Normal file
21
scripts/copy-ipm-libs.ps1
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
param(
|
||||||
|
[string]$SourceDir = "C:\ipm_binaries",
|
||||||
|
[switch]$Force
|
||||||
|
)
|
||||||
|
|
||||||
|
# Copies Windows DLLs from SourceDir to app/ipm/lib/windows.
|
||||||
|
$dest = Resolve-Path -Path "..\app\ipm\lib\windows"
|
||||||
|
if (-not (Test-Path $dest)) { New-Item -ItemType Directory -Path $dest -Force | Out-Null }
|
||||||
|
|
||||||
|
Write-Host "Copying DLLs from $SourceDir to $dest"
|
||||||
|
Get-ChildItem -Path $SourceDir -Filter *.dll -File -ErrorAction SilentlyContinue | ForEach-Object {
|
||||||
|
$dst = Join-Path $dest $_.Name
|
||||||
|
if (Test-Path $dst -and -not $Force) {
|
||||||
|
Write-Host "Skipping existing: $($_.Name)"
|
||||||
|
} else {
|
||||||
|
Copy-Item $_.FullName -Destination $dst -Force
|
||||||
|
Write-Host "Copied: $($_.Name)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Done."
|
||||||
14
scripts/copy-ipm-libs.sh
Normal file
14
scripts/copy-ipm-libs.sh
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SOURCE_DIR=${1:-/opt/ipm_binaries}
|
||||||
|
DEST_DIR="$(dirname "$0")/../app/ipm/lib/linux"
|
||||||
|
|
||||||
|
mkdir -p "$DEST_DIR"
|
||||||
|
echo "Copying .so files from $SOURCE_DIR to $DEST_DIR"
|
||||||
|
shopt -s nullglob
|
||||||
|
for f in "$SOURCE_DIR"/*.so; do
|
||||||
|
echo "Copying $(basename "$f")"
|
||||||
|
cp -f "$f" "$DEST_DIR/"
|
||||||
|
done
|
||||||
|
echo "Done."
|
||||||
58
scripts/run_api_tests.py
Normal file
58
scripts/run_api_tests.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import requests, json, base64, os
|
||||||
|
base = 'http://127.0.0.1:8001'
|
||||||
|
print('Health ->', requests.get(base + '/api/v1/health').json())
|
||||||
|
# Diagram JSON
|
||||||
|
body = {
|
||||||
|
'refrigerant': 'R290',
|
||||||
|
'pressure_range': {'min': 0.1, 'max': 10.0},
|
||||||
|
'format': 'json',
|
||||||
|
'include_isotherms': True,
|
||||||
|
'width': 800,
|
||||||
|
'height': 600,
|
||||||
|
'dpi': 100
|
||||||
|
}
|
||||||
|
print('\nRequesting diagram JSON...')
|
||||||
|
r = requests.post(base + '/api/v1/diagrams/ph', json=body, timeout=60)
|
||||||
|
print('Status', r.status_code)
|
||||||
|
try:
|
||||||
|
j = r.json()
|
||||||
|
print('Keys in response:', list(j.keys()))
|
||||||
|
if 'data' in j:
|
||||||
|
print('Saturation curve length:', len(j['data'].get('saturation_curve', [])))
|
||||||
|
except Exception as e:
|
||||||
|
print('Failed to parse JSON:', e, r.text[:200])
|
||||||
|
|
||||||
|
# Diagram PNG
|
||||||
|
body['format'] = 'png'
|
||||||
|
print('\nRequesting diagram PNG...')
|
||||||
|
r2 = requests.post(base + '/api/v1/diagrams/ph', json=body, timeout=60)
|
||||||
|
print('Status', r2.status_code)
|
||||||
|
try:
|
||||||
|
j2 = r2.json()
|
||||||
|
print('Keys in response:', list(j2.keys()))
|
||||||
|
if 'image' in j2:
|
||||||
|
img_b64 = j2['image']
|
||||||
|
os.makedirs('test_outputs', exist_ok=True)
|
||||||
|
path = os.path.join('test_outputs','sample_diagram.png')
|
||||||
|
with open(path, 'wb') as f:
|
||||||
|
f.write(base64.b64decode(img_b64))
|
||||||
|
print('Saved PNG to', path)
|
||||||
|
except ValueError:
|
||||||
|
print('Response is not JSON, printing text length', len(r2.text))
|
||||||
|
|
||||||
|
# Simple cycle (pressure mode)
|
||||||
|
print('\nRequesting simple cycle...')
|
||||||
|
cycle_body = {
|
||||||
|
'refrigerant': 'R290',
|
||||||
|
'evap_pressure': 0.2, # bar
|
||||||
|
'cond_pressure': 6.0, # bar
|
||||||
|
'superheat': 5.0,
|
||||||
|
'subcool': 2.0,
|
||||||
|
'mass_flow': 0.1
|
||||||
|
}
|
||||||
|
r3 = requests.post(base + '/api/v1/cycles/simple', json=cycle_body, timeout=60)
|
||||||
|
print('Status', r3.status_code)
|
||||||
|
try:
|
||||||
|
print('Simple cycle keys:', list(r3.json().keys()))
|
||||||
|
except Exception as e:
|
||||||
|
print('Failed to parse cycle response:', e, r3.text[:200])
|
||||||
78
scripts/run_api_tests_docker.py
Normal file
78
scripts/run_api_tests_docker.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import requests, json, base64, os
|
||||||
|
base = 'http://127.0.0.1:8002'
|
||||||
|
print('Health ->', requests.get(base + '/api/v1/health').json())
|
||||||
|
# Diagram JSON
|
||||||
|
body = {
|
||||||
|
'refrigerant': 'R290',
|
||||||
|
'pressure_range': {'min': 0.1, 'max': 10.0},
|
||||||
|
'format': 'json',
|
||||||
|
'include_isotherms': True,
|
||||||
|
'width': 800,
|
||||||
|
'height': 600,
|
||||||
|
'dpi': 100
|
||||||
|
}
|
||||||
|
print('\nRequesting diagram JSON...')
|
||||||
|
r = requests.post(base + '/api/v1/diagrams/ph', json=body, timeout=120)
|
||||||
|
print('Status', r.status_code)
|
||||||
|
try:
|
||||||
|
j = r.json()
|
||||||
|
print('Keys in response:', list(j.keys()))
|
||||||
|
if 'data' in j:
|
||||||
|
print('Saturation curve length:', len(j['data'].get('saturation_curve', [])))
|
||||||
|
except Exception as e:
|
||||||
|
print('Failed to parse JSON:', e, r.text[:200])
|
||||||
|
|
||||||
|
# Diagram PNG
|
||||||
|
body['format'] = 'png'
|
||||||
|
print('\nRequesting diagram PNG...')
|
||||||
|
r2 = requests.post(base + '/api/v1/diagrams/ph', json=body, timeout=120)
|
||||||
|
print('Status', r2.status_code)
|
||||||
|
try:
|
||||||
|
j2 = r2.json()
|
||||||
|
print('Keys in response:', list(j2.keys()))
|
||||||
|
if 'image' in j2:
|
||||||
|
img_b64 = j2['image']
|
||||||
|
os.makedirs('test_outputs', exist_ok=True)
|
||||||
|
path = os.path.join('test_outputs','docker_sample_diagram.png')
|
||||||
|
with open(path, 'wb') as f:
|
||||||
|
f.write(base64.b64decode(img_b64))
|
||||||
|
print('Saved PNG to', path)
|
||||||
|
except Exception as e:
|
||||||
|
print('Failed to parse PNG response:', e, r2.text[:200])
|
||||||
|
|
||||||
|
# Simple cycle (pressure mode)
|
||||||
|
print('\nRequesting simple cycle (pressure mode)...')
|
||||||
|
cycle_body = {
|
||||||
|
'refrigerant': 'R290',
|
||||||
|
'evap_pressure': 0.2, # bar
|
||||||
|
'cond_pressure': 6.0, # bar
|
||||||
|
'superheat': 5.0,
|
||||||
|
'subcool': 2.0,
|
||||||
|
'mass_flow': 0.1
|
||||||
|
}
|
||||||
|
r3 = requests.post(base + '/api/v1/cycles/simple', json=cycle_body, timeout=120)
|
||||||
|
print('Status', r3.status_code)
|
||||||
|
try:
|
||||||
|
print('Simple cycle keys:', list(r3.json().keys()))
|
||||||
|
except Exception as e:
|
||||||
|
print('Failed to parse cycle response:', e, r3.text[:200])
|
||||||
|
|
||||||
|
# Simple cycle (temperature mode)
|
||||||
|
print('\nRequesting simple cycle (temperature mode)...')
|
||||||
|
cycle_body2 = {
|
||||||
|
'refrigerant': 'R290',
|
||||||
|
'evap_temperature': -10.0,
|
||||||
|
'cond_temperature': 40.0,
|
||||||
|
'superheat': 5.0,
|
||||||
|
'subcool': 2.0,
|
||||||
|
'mass_flow': 0.1
|
||||||
|
}
|
||||||
|
r4 = requests.post(base + '/api/v1/cycles/simple', json=cycle_body2, timeout=120)
|
||||||
|
print('Status', r4.status_code)
|
||||||
|
try:
|
||||||
|
j4 = r4.json()
|
||||||
|
print('Keys:', list(j4.keys()))
|
||||||
|
if 'performance' in j4:
|
||||||
|
print('COP:', j4['performance'].get('cop'))
|
||||||
|
except Exception as e:
|
||||||
|
print('Failed to parse cycle temp response:', e, r4.text[:200])
|
||||||
25
scripts/test_cycle_temp.py
Normal file
25
scripts/test_cycle_temp.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import requests, json
|
||||||
|
base = 'http://127.0.0.1:8001'
|
||||||
|
print('Health ->', requests.get(base + '/api/v1/health').json())
|
||||||
|
|
||||||
|
body = {
|
||||||
|
'refrigerant': 'R290',
|
||||||
|
'evap_temperature': -10.0, # °C
|
||||||
|
'cond_temperature': 40.0, # °C
|
||||||
|
'superheat': 5.0,
|
||||||
|
'subcool': 2.0,
|
||||||
|
'mass_flow': 0.1
|
||||||
|
}
|
||||||
|
print('\nRequesting simple cycle (temperature mode)...')
|
||||||
|
r = requests.post(base + '/api/v1/cycles/simple', json=body, timeout=60)
|
||||||
|
print('Status', r.status_code)
|
||||||
|
try:
|
||||||
|
j = r.json()
|
||||||
|
print('Keys:', list(j.keys()))
|
||||||
|
if 'performance' in j:
|
||||||
|
print('COP:', j['performance'].get('cop'))
|
||||||
|
print('Compressor efficiency:', j['performance'].get('compressor_efficiency'))
|
||||||
|
if 'diagram_data' in j:
|
||||||
|
print('Diagram cycle points count:', len(j['diagram_data'].get('cycle_points', [])))
|
||||||
|
except Exception as e:
|
||||||
|
print('Failed to parse JSON:', e, r.text[:200])
|
||||||
28
scripts/test_economizer.py
Normal file
28
scripts/test_economizer.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Ensure project root is on sys.path so 'app' package is importable when running scripts
|
||||||
|
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
if ROOT not in sys.path:
|
||||||
|
sys.path.insert(0, ROOT)
|
||||||
|
|
||||||
|
from app.core.refrigerant_loader import RefrigerantLibrary
|
||||||
|
from app.services.cycle_calculator import CycleCalculator
|
||||||
|
|
||||||
|
# Create refrigerant and calculator
|
||||||
|
refrig = RefrigerantLibrary('R290')
|
||||||
|
calc = CycleCalculator(refrig)
|
||||||
|
|
||||||
|
# Typical pressures in Pa (convert from bar)
|
||||||
|
evap = 0.2 * 1e5
|
||||||
|
cond = 6.0 * 1e5
|
||||||
|
inter = 2.0 * 1e5
|
||||||
|
|
||||||
|
res = calc.calculate_cycle_with_economizer(evap, cond, inter, superheat=5.0, subcool=3.0, mass_flow=0.1)
|
||||||
|
|
||||||
|
print('Economizer result keys:', res.keys())
|
||||||
|
print('Flash fraction:', res['performance'].get('flash_fraction'))
|
||||||
|
print('COP:', res['performance'].get('cop'))
|
||||||
|
print('Points:')
|
||||||
|
for p in res['points']:
|
||||||
|
print(' -', p)
|
||||||
5
start_api.sh
Executable file
5
start_api.sh
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
export LD_LIBRARY_PATH="$(pwd)/app/ipm/lib/so:${LD_LIBRARY_PATH:-}"
|
||||||
|
export PYTHONPATH="$(pwd)"
|
||||||
|
. .venv/bin/activate
|
||||||
|
uvicorn app.main:app --host 127.0.0.1 --port 8001
|
||||||
BIN
test_complete.png
Normal file
BIN
test_complete.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 232 KiB |
BIN
test_matplotlib.png
Normal file
BIN
test_matplotlib.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
test_outputs/sample_diagram.png
Normal file
BIN
test_outputs/sample_diagram.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 147 KiB |
BIN
test_saturation.png
Normal file
BIN
test_saturation.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
44
tests/test_diagram_api.py
Normal file
44
tests/test_diagram_api.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import base64
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.main import app
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_diagram_json_no_image():
|
||||||
|
body = {
|
||||||
|
'refrigerant': 'R290',
|
||||||
|
'pressure_range': {'min': 0.1, 'max': 10.0},
|
||||||
|
'format': 'json',
|
||||||
|
'include_isotherms': True,
|
||||||
|
'width': 800,
|
||||||
|
'height': 600,
|
||||||
|
'dpi': 100
|
||||||
|
}
|
||||||
|
r = client.post('/api/v1/diagrams/ph', json=body)
|
||||||
|
assert r.status_code == 200
|
||||||
|
j = r.json()
|
||||||
|
# image key should not be present for json-only
|
||||||
|
assert 'image' not in j
|
||||||
|
assert 'data' in j
|
||||||
|
|
||||||
|
|
||||||
|
def test_diagram_png_includes_image():
|
||||||
|
body = {
|
||||||
|
'refrigerant': 'R290',
|
||||||
|
'pressure_range': {'min': 0.1, 'max': 10.0},
|
||||||
|
'format': 'png',
|
||||||
|
'include_isotherms': True,
|
||||||
|
'width': 800,
|
||||||
|
'height': 600,
|
||||||
|
'dpi': 100
|
||||||
|
}
|
||||||
|
r = client.post('/api/v1/diagrams/ph', json=body)
|
||||||
|
assert r.status_code == 200
|
||||||
|
j = r.json()
|
||||||
|
assert 'image' in j
|
||||||
|
# Validate base64 decodes
|
||||||
|
decoded = base64.b64decode(j['image'])
|
||||||
|
assert len(decoded) > 10
|
||||||
19
tests/test_economizer_unit.py
Normal file
19
tests/test_economizer_unit.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import sys, os
|
||||||
|
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
if ROOT not in sys.path:
|
||||||
|
sys.path.insert(0, ROOT)
|
||||||
|
|
||||||
|
from app.core.refrigerant_loader import RefrigerantLibrary
|
||||||
|
from app.services.cycle_calculator import CycleCalculator
|
||||||
|
|
||||||
|
|
||||||
|
def test_economizer_runs():
|
||||||
|
refrigerant = RefrigerantLibrary('R290')
|
||||||
|
calc = CycleCalculator(refrigerant)
|
||||||
|
evap = 0.2 * 1e5
|
||||||
|
cond = 6.0 * 1e5
|
||||||
|
inter = 2.0 * 1e5
|
||||||
|
res = calc.calculate_cycle_with_economizer(evap, cond, inter, mass_flow=0.1)
|
||||||
|
assert 'performance' in res
|
||||||
|
assert 'flash_fraction' in res['performance']
|
||||||
|
assert 0.0 <= res['performance']['flash_fraction'] <= 1.0
|
||||||
@@ -201,8 +201,13 @@ def test_api_direct():
|
|||||||
try:
|
try:
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
# Charger la requête
|
# Charger la requête (fichier à côté de ce script)
|
||||||
with open('request_r290.json', 'r') as f:
|
request_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'request_r290.json')
|
||||||
|
if not os.path.exists(request_path):
|
||||||
|
# Fallback: essayer dans le répertoire courant
|
||||||
|
request_path = 'request_r290.json'
|
||||||
|
|
||||||
|
with open(request_path, 'r') as f:
|
||||||
request_data = json.load(f)
|
request_data = json.load(f)
|
||||||
|
|
||||||
# Appeler l'API
|
# Appeler l'API
|
||||||
|
|||||||
Reference in New Issue
Block a user