Commit iniziale
This commit is contained in:
@@ -0,0 +1,63 @@
|
|||||||
|
###############################################################################
|
||||||
|
# Set default behavior to automatically normalize line endings.
|
||||||
|
###############################################################################
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Set default behavior for command prompt diff.
|
||||||
|
#
|
||||||
|
# This is need for earlier builds of msysgit that does not have it on by
|
||||||
|
# default for csharp files.
|
||||||
|
# Note: This is only used by command line
|
||||||
|
###############################################################################
|
||||||
|
#*.cs diff=csharp
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Set the merge driver for project and solution files
|
||||||
|
#
|
||||||
|
# Merging from the command prompt will add diff markers to the files if there
|
||||||
|
# are conflicts (Merging from VS is not affected by the settings below, in VS
|
||||||
|
# the diff markers are never inserted). Diff markers may cause the following
|
||||||
|
# file extensions to fail to load in VS. An alternative would be to treat
|
||||||
|
# these files as binary and thus will always conflict and require user
|
||||||
|
# intervention with every merge. To do so, just uncomment the entries below
|
||||||
|
###############################################################################
|
||||||
|
#*.sln merge=binary
|
||||||
|
#*.csproj merge=binary
|
||||||
|
#*.vbproj merge=binary
|
||||||
|
#*.vcxproj merge=binary
|
||||||
|
#*.vcproj merge=binary
|
||||||
|
#*.dbproj merge=binary
|
||||||
|
#*.fsproj merge=binary
|
||||||
|
#*.lsproj merge=binary
|
||||||
|
#*.wixproj merge=binary
|
||||||
|
#*.modelproj merge=binary
|
||||||
|
#*.sqlproj merge=binary
|
||||||
|
#*.wwaproj merge=binary
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# behavior for image files
|
||||||
|
#
|
||||||
|
# image files are treated as binary by default.
|
||||||
|
###############################################################################
|
||||||
|
#*.jpg binary
|
||||||
|
#*.png binary
|
||||||
|
#*.gif binary
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# diff behavior for common document formats
|
||||||
|
#
|
||||||
|
# Convert binary document formats to text before diffing them. This feature
|
||||||
|
# is only available from the command line. Turn it on by uncommenting the
|
||||||
|
# entries below.
|
||||||
|
###############################################################################
|
||||||
|
#*.doc diff=astextplain
|
||||||
|
#*.DOC diff=astextplain
|
||||||
|
#*.docx diff=astextplain
|
||||||
|
#*.DOCX diff=astextplain
|
||||||
|
#*.dot diff=astextplain
|
||||||
|
#*.DOT diff=astextplain
|
||||||
|
#*.pdf diff=astextplain
|
||||||
|
#*.PDF diff=astextplain
|
||||||
|
#*.rtf diff=astextplain
|
||||||
|
#*.RTF diff=astextplain
|
||||||
+375
@@ -0,0 +1,375 @@
|
|||||||
|
## Ignore Visual Studio temporary files, build results, and
|
||||||
|
## files generated by popular Visual Studio add-ons.
|
||||||
|
##
|
||||||
|
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.rsuser
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||||
|
*.userprefs
|
||||||
|
|
||||||
|
# Mono auto generated files
|
||||||
|
mono_crash.*
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
[Dd]ebug/
|
||||||
|
[Dd]ebugPublic/
|
||||||
|
[Rr]elease/
|
||||||
|
[Rr]eleases/
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
[Ww][Ii][Nn]32/
|
||||||
|
[Aa][Rr][Mm]/
|
||||||
|
[Aa][Rr][Mm]64/
|
||||||
|
bld/
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
[Oo]ut/
|
||||||
|
[Ll]og/
|
||||||
|
[Ll]ogs/
|
||||||
|
|
||||||
|
# Visual Studio 2015/2017 cache/options directory
|
||||||
|
.vs/
|
||||||
|
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||||
|
#wwwroot/
|
||||||
|
|
||||||
|
# Visual Studio 2017 auto generated files
|
||||||
|
Generated\ Files/
|
||||||
|
|
||||||
|
# MSTest test Results
|
||||||
|
[Tt]est[Rr]esult*/
|
||||||
|
[Bb]uild[Ll]og.*
|
||||||
|
|
||||||
|
# NUnit
|
||||||
|
*.VisualState.xml
|
||||||
|
TestResult.xml
|
||||||
|
nunit-*.xml
|
||||||
|
|
||||||
|
# Build Results of an ATL Project
|
||||||
|
[Dd]ebugPS/
|
||||||
|
[Rr]eleasePS/
|
||||||
|
dlldata.c
|
||||||
|
|
||||||
|
# Benchmark Results
|
||||||
|
BenchmarkDotNet.Artifacts/
|
||||||
|
|
||||||
|
# .NET Core
|
||||||
|
project.lock.json
|
||||||
|
project.fragment.lock.json
|
||||||
|
artifacts/
|
||||||
|
|
||||||
|
# ASP.NET Scaffolding
|
||||||
|
ScaffoldingReadMe.txt
|
||||||
|
|
||||||
|
# StyleCop
|
||||||
|
StyleCopReport.xml
|
||||||
|
|
||||||
|
# Files built by Visual Studio
|
||||||
|
*_i.c
|
||||||
|
*_p.c
|
||||||
|
*_h.h
|
||||||
|
*.ilk
|
||||||
|
*.meta
|
||||||
|
*.obj
|
||||||
|
*.iobj
|
||||||
|
*.pch
|
||||||
|
*.pdb
|
||||||
|
*.ipdb
|
||||||
|
*.pgc
|
||||||
|
*.pgd
|
||||||
|
*.rsp
|
||||||
|
*.sbr
|
||||||
|
*.tlb
|
||||||
|
*.tli
|
||||||
|
*.tlh
|
||||||
|
*.tmp
|
||||||
|
*.tmp_proj
|
||||||
|
*_wpftmp.csproj
|
||||||
|
*.log
|
||||||
|
*.vspscc
|
||||||
|
*.vssscc
|
||||||
|
.builds
|
||||||
|
*.pidb
|
||||||
|
*.svclog
|
||||||
|
*.scc
|
||||||
|
.venv
|
||||||
|
|
||||||
|
# Chutzpah Test files
|
||||||
|
_Chutzpah*
|
||||||
|
|
||||||
|
# Visual C++ cache files
|
||||||
|
ipch/
|
||||||
|
*.aps
|
||||||
|
*.ncb
|
||||||
|
*.opendb
|
||||||
|
*.opensdf
|
||||||
|
*.sdf
|
||||||
|
*.cachefile
|
||||||
|
*.VC.db
|
||||||
|
*.VC.VC.opendb
|
||||||
|
|
||||||
|
# Visual Studio profiler
|
||||||
|
*.psess
|
||||||
|
*.vsp
|
||||||
|
*.vspx
|
||||||
|
*.sap
|
||||||
|
|
||||||
|
# Visual Studio Trace Files
|
||||||
|
*.e2e
|
||||||
|
|
||||||
|
# TFS 2012 Local Workspace
|
||||||
|
$tf/
|
||||||
|
|
||||||
|
# Guidance Automation Toolkit
|
||||||
|
*.gpState
|
||||||
|
|
||||||
|
# ReSharper is a .NET coding add-in
|
||||||
|
_ReSharper*/
|
||||||
|
*.[Rr]e[Ss]harper
|
||||||
|
*.DotSettings.user
|
||||||
|
|
||||||
|
# TeamCity is a build add-in
|
||||||
|
_TeamCity*
|
||||||
|
|
||||||
|
# DotCover is a Code Coverage Tool
|
||||||
|
*.dotCover
|
||||||
|
|
||||||
|
# AxoCover is a Code Coverage Tool
|
||||||
|
.axoCover/*
|
||||||
|
!.axoCover/settings.json
|
||||||
|
|
||||||
|
# Coverlet is a free, cross platform Code Coverage Tool
|
||||||
|
coverage*.json
|
||||||
|
coverage*.xml
|
||||||
|
coverage*.info
|
||||||
|
|
||||||
|
# Visual Studio code coverage results
|
||||||
|
*.coverage
|
||||||
|
*.coveragexml
|
||||||
|
|
||||||
|
# NCrunch
|
||||||
|
_NCrunch_*
|
||||||
|
.*crunch*.local.xml
|
||||||
|
nCrunchTemp_*
|
||||||
|
|
||||||
|
# MightyMoose
|
||||||
|
*.mm.*
|
||||||
|
AutoTest.Net/
|
||||||
|
|
||||||
|
# Web workbench (sass)
|
||||||
|
.sass-cache/
|
||||||
|
|
||||||
|
# Installshield output folder
|
||||||
|
[Ee]xpress/
|
||||||
|
|
||||||
|
# DocProject is a documentation generator add-in
|
||||||
|
DocProject/buildhelp/
|
||||||
|
DocProject/Help/*.HxT
|
||||||
|
DocProject/Help/*.HxC
|
||||||
|
DocProject/Help/*.hhc
|
||||||
|
DocProject/Help/*.hhk
|
||||||
|
DocProject/Help/*.hhp
|
||||||
|
DocProject/Help/Html2
|
||||||
|
DocProject/Help/html
|
||||||
|
|
||||||
|
# Click-Once directory
|
||||||
|
publish/
|
||||||
|
|
||||||
|
# Publish Web Output
|
||||||
|
*.[Pp]ublish.xml
|
||||||
|
*.azurePubxml
|
||||||
|
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||||
|
# but database connection strings (with potential passwords) will be unencrypted
|
||||||
|
*.pubxml
|
||||||
|
*.publishproj
|
||||||
|
|
||||||
|
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||||
|
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||||
|
# in these scripts will be unencrypted
|
||||||
|
PublishScripts/
|
||||||
|
|
||||||
|
# NuGet Packages
|
||||||
|
*.nupkg
|
||||||
|
# NuGet Symbol Packages
|
||||||
|
*.snupkg
|
||||||
|
# The packages folder can be ignored because of Package Restore
|
||||||
|
**/[Pp]ackages/*
|
||||||
|
# except build/, which is used as an MSBuild target.
|
||||||
|
!**/[Pp]ackages/build/
|
||||||
|
# Uncomment if necessary however generally it will be regenerated when needed
|
||||||
|
#!**/[Pp]ackages/repositories.config
|
||||||
|
# NuGet v3's project.json files produces more ignorable files
|
||||||
|
*.nuget.props
|
||||||
|
*.nuget.targets
|
||||||
|
|
||||||
|
# Microsoft Azure Build Output
|
||||||
|
csx/
|
||||||
|
*.build.csdef
|
||||||
|
|
||||||
|
# Microsoft Azure Emulator
|
||||||
|
ecf/
|
||||||
|
rcf/
|
||||||
|
|
||||||
|
# Windows Store app package directories and files
|
||||||
|
AppPackages/
|
||||||
|
BundleArtifacts/
|
||||||
|
Package.StoreAssociation.xml
|
||||||
|
_pkginfo.txt
|
||||||
|
*.appx
|
||||||
|
*.appxbundle
|
||||||
|
*.appxupload
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!?*.[Cc]ache/
|
||||||
|
|
||||||
|
# Others
|
||||||
|
ClientBin/
|
||||||
|
~$*
|
||||||
|
*~
|
||||||
|
*.dbmdl
|
||||||
|
*.dbproj.schemaview
|
||||||
|
*.jfm
|
||||||
|
*.pfx
|
||||||
|
*.publishsettings
|
||||||
|
orleans.codegen.cs
|
||||||
|
|
||||||
|
# Including strong name files can present a security risk
|
||||||
|
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||||
|
#*.snk
|
||||||
|
|
||||||
|
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||||
|
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||||
|
#bower_components/
|
||||||
|
|
||||||
|
# RIA/Silverlight projects
|
||||||
|
Generated_Code/
|
||||||
|
|
||||||
|
# Backup & report files from converting an old project file
|
||||||
|
# to a newer Visual Studio version. Backup files are not needed,
|
||||||
|
# because we have git ;-)
|
||||||
|
_UpgradeReport_Files/
|
||||||
|
Backup*/
|
||||||
|
UpgradeLog*.XML
|
||||||
|
UpgradeLog*.htm
|
||||||
|
ServiceFabricBackup/
|
||||||
|
*.rptproj.bak
|
||||||
|
|
||||||
|
# SQL Server files
|
||||||
|
*.mdf
|
||||||
|
*.ldf
|
||||||
|
*.ndf
|
||||||
|
|
||||||
|
# Business Intelligence projects
|
||||||
|
*.rdl.data
|
||||||
|
*.bim.layout
|
||||||
|
*.bim_*.settings
|
||||||
|
*.rptproj.rsuser
|
||||||
|
*- [Bb]ackup.rdl
|
||||||
|
*- [Bb]ackup ([0-9]).rdl
|
||||||
|
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||||
|
|
||||||
|
# Microsoft Fakes
|
||||||
|
FakesAssemblies/
|
||||||
|
|
||||||
|
# GhostDoc plugin setting file
|
||||||
|
*.GhostDoc.xml
|
||||||
|
|
||||||
|
# Node.js Tools for Visual Studio
|
||||||
|
.ntvs_analysis.dat
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Visual Studio 6 build log
|
||||||
|
*.plg
|
||||||
|
|
||||||
|
# Visual Studio 6 workspace options file
|
||||||
|
*.opt
|
||||||
|
|
||||||
|
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||||
|
*.vbw
|
||||||
|
|
||||||
|
# Visual Studio LightSwitch build output
|
||||||
|
**/*.HTMLClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/ModelManifest.xml
|
||||||
|
**/*.Server/GeneratedArtifacts
|
||||||
|
**/*.Server/ModelManifest.xml
|
||||||
|
_Pvt_Extensions
|
||||||
|
|
||||||
|
# Paket dependency manager
|
||||||
|
.paket/paket.exe
|
||||||
|
paket-files/
|
||||||
|
|
||||||
|
# FAKE - F# Make
|
||||||
|
.fake/
|
||||||
|
|
||||||
|
# CodeRush personal settings
|
||||||
|
.cr/personal
|
||||||
|
|
||||||
|
# Python Tools for Visual Studio (PTVS)
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Cake - Uncomment if you are using it
|
||||||
|
# tools/**
|
||||||
|
# !tools/packages.config
|
||||||
|
|
||||||
|
# Tabs Studio
|
||||||
|
*.tss
|
||||||
|
|
||||||
|
# Telerik's JustMock configuration file
|
||||||
|
*.jmconfig
|
||||||
|
|
||||||
|
# BizTalk build output
|
||||||
|
*.btp.cs
|
||||||
|
*.btm.cs
|
||||||
|
*.odx.cs
|
||||||
|
*.xsd.cs
|
||||||
|
|
||||||
|
# OpenCover UI analysis results
|
||||||
|
OpenCover/
|
||||||
|
|
||||||
|
# Azure Stream Analytics local run output
|
||||||
|
ASALocalRun/
|
||||||
|
|
||||||
|
# MSBuild Binary and Structured Log
|
||||||
|
*.binlog
|
||||||
|
|
||||||
|
# NVidia Nsight GPU debugger configuration file
|
||||||
|
*.nvuser
|
||||||
|
|
||||||
|
# MFractors (Xamarin productivity tool) working folder
|
||||||
|
.mfractor/
|
||||||
|
|
||||||
|
# Local History for Visual Studio
|
||||||
|
.localhistory/
|
||||||
|
|
||||||
|
# BeatPulse healthcheck temp database
|
||||||
|
healthchecksdb
|
||||||
|
|
||||||
|
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||||
|
MigrationBackup/
|
||||||
|
|
||||||
|
# Ionide (cross platform F# VS Code tools) working folder
|
||||||
|
.ionide/
|
||||||
|
|
||||||
|
# Fody - auto-generated XML schema
|
||||||
|
FodyWeavers.xsd
|
||||||
|
|
||||||
|
data/
|
||||||
|
*.csv
|
||||||
|
|
||||||
|
# Ignoruj wyuczone modele (wagi)
|
||||||
|
models/
|
||||||
|
*.pth
|
||||||
|
|
||||||
|
# Ignoruj wygenerowane animacje
|
||||||
|
animations/
|
||||||
|
*.html
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**Heat Equation PINN** — A Physics-Informed Neural Network that solves the 1D time-varying heat equation with physical boundary conditions:
|
||||||
|
|
||||||
|
```
|
||||||
|
∂T/∂t = α ∂²T/∂x² x ∈ [0, L], t ∈ [0, T_END]
|
||||||
|
```
|
||||||
|
|
||||||
|
- **IC:** `T(x, 0) = T0` (uniform)
|
||||||
|
- **BC x=0:** Neumann — heat flux step: `−k ∂T/∂x = Q(t)` where `Q = Q_VAL` if `t ≥ T_STEP` else `0`
|
||||||
|
- **BC x=L:** Robin — convection: `−k ∂T/∂x = h (T − T_AMB)`
|
||||||
|
|
||||||
|
No experimental data is needed. A `fdm/` module provides a reference numerical solution (FTCS explicit scheme) used for evaluation and visualization comparison.
|
||||||
|
|
||||||
|
All physical and numerical parameters live in `config.py`.
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
Always activate the virtual environment first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source .venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
**PINN:**
|
||||||
|
```bash
|
||||||
|
python app.py # Train / Evaluate (L2 vs FDM) / Visualize
|
||||||
|
```
|
||||||
|
|
||||||
|
**FDM reference solver:**
|
||||||
|
```bash
|
||||||
|
python fdm/app.py # Solve / Heatmap / Animation / Time-series
|
||||||
|
```
|
||||||
|
|
||||||
|
Saved artifacts (git-ignored): `models/best_heat_pinn_model.pth`, HTML plots in `animations/` and `animations/fdm/`.
|
||||||
|
|
||||||
|
To retrain from scratch: `rm models/best_heat_pinn_model.pth` before running option 1.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
`requirements.txt` exists. Key packages: `torch`, `numpy`, `plotly`. No `pandas` or `scikit-learn` needed.
|
||||||
|
|
||||||
|
GPU is auto-detected (`cuda` → `mps` → `cpu`) in `engine.py:_get_device()`.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
config.py ← all physical + numerical parameters (edit here to change the problem)
|
||||||
|
app.py ← PINN CLI menu
|
||||||
|
model.py ← HeatPINN + heat_pinn_loss()
|
||||||
|
engine.py ← data sampling, Adam+L-BFGS training, evaluation vs FDM, visualization call
|
||||||
|
visualizer.py ← PINN vs FDM: heatmap, animated T(x), time-series at fixed points
|
||||||
|
fdm/
|
||||||
|
solver.py ← FTCS explicit scheme, ghost-cell Neumann, explicit Robin
|
||||||
|
visualizer.py ← same 3 plot types for FDM-only output
|
||||||
|
app.py ← FDM CLI menu
|
||||||
|
```
|
||||||
|
|
||||||
|
### Neural Network (`model.py`)
|
||||||
|
|
||||||
|
`HeatPINN`: 5-layer fully connected, input `(x, t)` → output `T`.
|
||||||
|
|
||||||
|
**Output scaling** — the network predicts a dimensionless perturbation; the `forward()` applies:
|
||||||
|
```
|
||||||
|
T = T_AMB + (Q_VAL · L / K) · net(x, t)
|
||||||
|
```
|
||||||
|
This keeps `net` outputs in `[0, 1]` range and ensures gradients `∂T/∂x` are O(1) for the network to learn. Do not remove this scaling.
|
||||||
|
|
||||||
|
`heat_pinn_loss()` normalizes all three loss terms to O(1) using `T_char = Q_VAL·L/K` and `grad_char = (Q_VAL/K)²`. Changing physical parameters in `config.py` does not require re-tuning loss weights.
|
||||||
|
|
||||||
|
### Training (`engine.py`)
|
||||||
|
|
||||||
|
`prepare_data()` samples collocation points with **deliberate clustering**: extra points near `x=0` (steep Neumann gradient) and around `t=T_STEP` (flux step discontinuity). Increasing `N_f` / `N_bc` here is the first lever to pull if accuracy is low.
|
||||||
|
|
||||||
|
`train_model()` runs **Adam first, then L-BFGS fine-tuning**. L-BFGS uses a closure that captures loss components in `_last` dict (avoids calling `heat_pinn_loss` outside an active grad context).
|
||||||
|
|
||||||
|
`evaluate_model()` runs the FDM solver and downsamples its `(NX, NT)` output to the PINN prediction grid `(100, 100)` for L2 comparison.
|
||||||
|
|
||||||
|
### FDM Solver (`fdm/solver.py`)
|
||||||
|
|
||||||
|
Returns `(T_matrix[NX, NT], x_vals, t_vals)`. Uses:
|
||||||
|
- Ghost cell for Neumann: `T_ghost = T[1] + 2·dx·Q(t)/k`
|
||||||
|
- Explicit Robin at x=L: `T[N] = (T[N−1] + dx·h/k·T_amb) / (1 + dx·h/k)` — uses `T_cur[-2]`, not `T_new[-2]`
|
||||||
|
- CFL check at startup (warns, does not crash)
|
||||||
|
|
||||||
|
### Loss Scaling Notes
|
||||||
|
|
||||||
|
If you change `Q_VAL`, `K`, `H_CONV`, or `L` in `config.py`, the normalization in `heat_pinn_loss()` adjusts automatically. If losses diverge, check that `T_char = Q_VAL·L/K` is not near zero.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import sys
|
||||||
|
import engine
|
||||||
|
|
||||||
|
|
||||||
|
def print_header():
|
||||||
|
print("=" * 55)
|
||||||
|
print(" Heat Equation PINN — ∂T/∂t = α ∂²T/∂x²")
|
||||||
|
print(" Neumann BC (x=0) + Robin BC (x=L)")
|
||||||
|
print("=" * 55)
|
||||||
|
|
||||||
|
|
||||||
|
def _ask_float(prompt, default):
|
||||||
|
val = input(prompt).strip()
|
||||||
|
try:
|
||||||
|
return float(val)
|
||||||
|
except ValueError:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _ask_int(prompt, default):
|
||||||
|
val = input(prompt).strip()
|
||||||
|
return int(val) if val.isdigit() else default
|
||||||
|
|
||||||
|
|
||||||
|
def main_menu():
|
||||||
|
print("\nInitializing collocation points...")
|
||||||
|
data = engine.prepare_data()
|
||||||
|
print(f"Ready — device: {data['device']}\n")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
print("\n" + "-" * 30)
|
||||||
|
print(" MAIN MENU")
|
||||||
|
print("-" * 30)
|
||||||
|
print("1. Train New Model")
|
||||||
|
print("2. Evaluate Model (L2 vs analytical)")
|
||||||
|
print("3. Visualize Temperature Field")
|
||||||
|
print("0. Exit")
|
||||||
|
print("-" * 30)
|
||||||
|
|
||||||
|
choice = input("Select an option (0-3): ").strip()
|
||||||
|
|
||||||
|
if choice == '1':
|
||||||
|
epochs = _ask_int("Epochs (default 5000): ", 5000)
|
||||||
|
engine.train_model(data, epochs=epochs)
|
||||||
|
|
||||||
|
elif choice == '2':
|
||||||
|
engine.evaluate_model(data)
|
||||||
|
|
||||||
|
elif choice == '3':
|
||||||
|
engine.generate_visualization(data)
|
||||||
|
|
||||||
|
elif choice == '0':
|
||||||
|
print("Exiting.")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("Invalid choice.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print_header()
|
||||||
|
main_menu()
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Fisica
|
||||||
|
ALPHA = 0.01 # diffusività termica [m²/s]
|
||||||
|
K = 1.0 # conducibilità termica [W/m·K]
|
||||||
|
L = 1.0 # lunghezza barra [m]
|
||||||
|
T0 = 20.0 # temperatura iniziale uniforme [°C]
|
||||||
|
|
||||||
|
# Sorgente a x=0 (Neumann step)
|
||||||
|
Q_VAL = 150.0 # flusso di calore applicato [W/m²]
|
||||||
|
T_STEP = 0.2 # istante di attivazione flusso [s]
|
||||||
|
|
||||||
|
# Convezione a x=L (Robin)
|
||||||
|
H_CONV = 10.0 # coefficiente convettivo [W/m²·K]
|
||||||
|
T_AMB = 20.0 # temperatura ambiente [°C]
|
||||||
|
|
||||||
|
# Dominio temporale
|
||||||
|
T_END = 10.0 # fine simulazione [s]
|
||||||
|
|
||||||
|
# Griglia FDM
|
||||||
|
NX = 100 # nodi spaziali
|
||||||
|
NT = 5000 # passi temporali (verifica CFL automatica)
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
import os
|
||||||
|
import random
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
import torch.optim as optim
|
||||||
|
from torch.optim.lr_scheduler import ReduceLROnPlateau
|
||||||
|
|
||||||
|
import config
|
||||||
|
from model import HeatPINN, heat_pinn_loss
|
||||||
|
from visualizer import visualize_heat_field
|
||||||
|
|
||||||
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
MODELS_DIR = os.path.join(BASE_DIR, 'models')
|
||||||
|
MODEL_SAVE_PATH = os.path.join(MODELS_DIR, 'best_heat_pinn_model.pth')
|
||||||
|
|
||||||
|
|
||||||
|
def set_seed(seed=42):
|
||||||
|
random.seed(seed)
|
||||||
|
np.random.seed(seed)
|
||||||
|
torch.manual_seed(seed)
|
||||||
|
if torch.cuda.is_available():
|
||||||
|
torch.cuda.manual_seed_all(seed)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_device():
|
||||||
|
if torch.cuda.is_available():
|
||||||
|
try:
|
||||||
|
t = torch.zeros(2, 2).cuda()
|
||||||
|
torch.mm(t, t)
|
||||||
|
return torch.device('cuda')
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
if torch.backends.mps.is_available():
|
||||||
|
return torch.device('mps')
|
||||||
|
return torch.device('cpu')
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_data(N_f=4000, N_ic=400, N_bc=400):
|
||||||
|
set_seed(42)
|
||||||
|
device = _get_device()
|
||||||
|
|
||||||
|
# Uniform collocation points
|
||||||
|
x_f = torch.rand(N_f, device=device) * config.L
|
||||||
|
t_f = torch.rand(N_f, device=device) * config.T_END
|
||||||
|
|
||||||
|
# Extra points near x=0 (steep Neumann gradient) and t=T_STEP (flux step)
|
||||||
|
n_extra = N_f // 4
|
||||||
|
x_near0 = torch.rand(n_extra, device=device) * config.L * 0.1 # x in [0, 0.1]
|
||||||
|
t_near0 = torch.rand(n_extra, device=device) * config.T_END
|
||||||
|
x_step = torch.rand(n_extra, device=device) * config.L
|
||||||
|
t_step = config.T_STEP + (torch.rand(n_extra, device=device) - 0.5) * 0.1 # t near T_STEP
|
||||||
|
t_step = t_step.clamp(0, config.T_END)
|
||||||
|
|
||||||
|
x_f = torch.cat([x_f, x_near0, x_step])
|
||||||
|
t_f = torch.cat([t_f, t_near0, t_step])
|
||||||
|
|
||||||
|
x_ic = torch.rand(N_ic, device=device) * config.L
|
||||||
|
t_bc = torch.rand(N_bc, device=device) * config.T_END
|
||||||
|
|
||||||
|
return {'device': device, 'x_f': x_f, 't_f': t_f, 'x_ic': x_ic, 't_bc': t_bc}
|
||||||
|
|
||||||
|
|
||||||
|
def train_model(data, epochs=5000, patience=100):
|
||||||
|
device = data['device']
|
||||||
|
model = HeatPINN().to(device)
|
||||||
|
optimizer = optim.Adam(model.parameters(), lr=1e-3)
|
||||||
|
scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=30, min_lr=1e-6)
|
||||||
|
|
||||||
|
os.makedirs(MODELS_DIR, exist_ok=True)
|
||||||
|
best_loss = float('inf')
|
||||||
|
patience_counter = 0
|
||||||
|
|
||||||
|
print(f"\n--- Heat PINN Training (Adam) on {device} ---")
|
||||||
|
for epoch in range(epochs):
|
||||||
|
model.train()
|
||||||
|
optimizer.zero_grad()
|
||||||
|
loss, L_pde, L_ic, L_bc = heat_pinn_loss(
|
||||||
|
model, data['x_f'], data['t_f'], data['x_ic'], data['t_bc']
|
||||||
|
)
|
||||||
|
loss.backward()
|
||||||
|
optimizer.step()
|
||||||
|
scheduler.step(loss.item())
|
||||||
|
|
||||||
|
if loss.item() < best_loss - 1e-7:
|
||||||
|
best_loss = loss.item()
|
||||||
|
patience_counter = 0
|
||||||
|
torch.save({'state_dict': model.state_dict()}, MODEL_SAVE_PATH)
|
||||||
|
else:
|
||||||
|
patience_counter += 1
|
||||||
|
|
||||||
|
if patience_counter >= patience:
|
||||||
|
print(f"Early stopping at epoch {epoch + 1}")
|
||||||
|
break
|
||||||
|
|
||||||
|
if (epoch + 1) % 100 == 0:
|
||||||
|
print(
|
||||||
|
f"Epoch [{epoch+1}/{epochs}] | Loss: {loss.item():.6f} "
|
||||||
|
f"| PDE: {L_pde.item():.6f} | IC: {L_ic.item():.6f} | BC: {L_bc.item():.6f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# L-BFGS fine-tuning phase (standard PINN practice for convergence to better minima)
|
||||||
|
print("\n--- L-BFGS fine-tuning ---")
|
||||||
|
ckpt = torch.load(MODEL_SAVE_PATH, map_location=device)
|
||||||
|
model.load_state_dict(ckpt['state_dict'])
|
||||||
|
|
||||||
|
lbfgs = optim.LBFGS(model.parameters(), lr=0.1, max_iter=50,
|
||||||
|
history_size=50, tolerance_grad=1e-7, line_search_fn='strong_wolfe')
|
||||||
|
|
||||||
|
_last = {}
|
||||||
|
|
||||||
|
for step in range(20):
|
||||||
|
def closure():
|
||||||
|
lbfgs.zero_grad()
|
||||||
|
loss, L_pde, L_ic, L_bc = heat_pinn_loss(
|
||||||
|
model, data['x_f'], data['t_f'], data['x_ic'], data['t_bc']
|
||||||
|
)
|
||||||
|
loss.backward()
|
||||||
|
_last['loss'] = loss.item()
|
||||||
|
_last['pde'] = L_pde.item()
|
||||||
|
_last['ic'] = L_ic.item()
|
||||||
|
_last['bc'] = L_bc.item()
|
||||||
|
return loss
|
||||||
|
|
||||||
|
lbfgs.step(closure)
|
||||||
|
if _last['loss'] < best_loss:
|
||||||
|
best_loss = _last['loss']
|
||||||
|
torch.save({'state_dict': model.state_dict()}, MODEL_SAVE_PATH)
|
||||||
|
if (step + 1) % 5 == 0:
|
||||||
|
print(f"L-BFGS step {step+1}/20 | Loss: {_last['loss']:.6f} "
|
||||||
|
f"| PDE: {_last['pde']:.6f} | IC: {_last['ic']:.6f} | BC: {_last['bc']:.6f}")
|
||||||
|
|
||||||
|
print("Training complete! Model saved.")
|
||||||
|
|
||||||
|
|
||||||
|
def _load_model(device):
|
||||||
|
if not os.path.exists(MODEL_SAVE_PATH):
|
||||||
|
print("Error: model not found. Train the model first.")
|
||||||
|
return None
|
||||||
|
ckpt = torch.load(MODEL_SAVE_PATH, map_location=device)
|
||||||
|
model = HeatPINN().to(device)
|
||||||
|
model.load_state_dict(ckpt['state_dict'])
|
||||||
|
model.eval()
|
||||||
|
return model
|
||||||
|
|
||||||
|
|
||||||
|
def _predict_grid(model, device, nx=100, nt=100):
|
||||||
|
x_vals = torch.linspace(0, config.L, nx, device=device)
|
||||||
|
t_vals = torch.linspace(0, config.T_END, nt, device=device)
|
||||||
|
xx, tt = torch.meshgrid(x_vals, t_vals, indexing='ij')
|
||||||
|
inp = torch.stack([xx.reshape(-1), tt.reshape(-1)], dim=1)
|
||||||
|
with torch.no_grad():
|
||||||
|
T_pred = model(inp).reshape(nx, nt).cpu().numpy()
|
||||||
|
return T_pred, x_vals.cpu().numpy(), t_vals.cpu().numpy()
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_model(data):
|
||||||
|
model = _load_model(data['device'])
|
||||||
|
if model is None:
|
||||||
|
return
|
||||||
|
T_pred, x_vals, t_vals = _predict_grid(model, data['device'])
|
||||||
|
|
||||||
|
# FDM reference
|
||||||
|
from fdm.solver import solve as fdm_solve
|
||||||
|
T_fdm, _, _ = fdm_solve()
|
||||||
|
|
||||||
|
# Downsample FDM time axis (NX=100, NT=5000) to match PINN grid (100x100)
|
||||||
|
nx, nt = T_pred.shape
|
||||||
|
t_indices = np.linspace(0, T_fdm.shape[1] - 1, nt, dtype=int)
|
||||||
|
T_fdm_ds = T_fdm[:, t_indices] # (100, 100)
|
||||||
|
|
||||||
|
l2_rel = np.sqrt(np.mean((T_pred - T_fdm_ds) ** 2)) / np.sqrt(np.mean(T_fdm_ds ** 2))
|
||||||
|
max_err = np.max(np.abs(T_pred - T_fdm_ds))
|
||||||
|
|
||||||
|
print(f"\n--- Evaluation vs FDM ---")
|
||||||
|
print(f"Relative L2 error vs FDM: {l2_rel:.6f}")
|
||||||
|
print(f"Max absolute error: {max_err:.6f} °C\n")
|
||||||
|
|
||||||
|
|
||||||
|
def generate_visualization(data):
|
||||||
|
model = _load_model(data['device'])
|
||||||
|
if model is None:
|
||||||
|
return
|
||||||
|
T_pred, x_vals, t_vals = _predict_grid(model, data['device'])
|
||||||
|
from fdm.solver import solve as fdm_solve
|
||||||
|
T_fdm, _, _ = fdm_solve()
|
||||||
|
visualize_heat_field(T_pred, x_vals, t_vals, T_fdm)
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from fdm.solver import solve
|
||||||
|
from fdm.visualizer import visualize_fdm
|
||||||
|
|
||||||
|
|
||||||
|
def print_header():
|
||||||
|
print("=" * 38)
|
||||||
|
print(" Heat Equation — FDM Solver")
|
||||||
|
print(" ∂T/∂t = α ∂²T/∂x²")
|
||||||
|
print("=" * 38)
|
||||||
|
|
||||||
|
|
||||||
|
def main_menu():
|
||||||
|
print("\nInitializing solver...")
|
||||||
|
print("Ready.\n")
|
||||||
|
|
||||||
|
T, x_vals, t_vals = None, None, None
|
||||||
|
|
||||||
|
while True:
|
||||||
|
print("\n" + "-" * 30)
|
||||||
|
print(" MAIN MENU")
|
||||||
|
print("-" * 30)
|
||||||
|
print("1. Risolvi e salva risultati")
|
||||||
|
print("2. Heatmap T(x,t)")
|
||||||
|
print("3. Animazione T(x) nel tempo")
|
||||||
|
print("4. Grafico T(t) in punti fissi")
|
||||||
|
print("0. Esci")
|
||||||
|
print("-" * 30)
|
||||||
|
|
||||||
|
choice = input("Select an option (0-4): ").strip()
|
||||||
|
|
||||||
|
if choice == "1":
|
||||||
|
T, x_vals, t_vals = solve()
|
||||||
|
print(f"Soluzione completata. Shape T: {T.shape}")
|
||||||
|
print(f"T range: [{T.min():.2f}, {T.max():.2f}] °C")
|
||||||
|
|
||||||
|
elif choice in ("2", "3", "4"):
|
||||||
|
if T is None:
|
||||||
|
print("Eseguire prima l'opzione 1.")
|
||||||
|
else:
|
||||||
|
visualize_fdm(T, x_vals, t_vals)
|
||||||
|
print("Grafici salvati in animations/fdm/")
|
||||||
|
|
||||||
|
elif choice == "0":
|
||||||
|
print("Uscita.")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("Scelta non valida.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print_header()
|
||||||
|
main_menu()
|
||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
"""
|
||||||
|
FTCS (Forward-Time Centered-Space) explicit finite difference solver
|
||||||
|
for the 1D heat equation:
|
||||||
|
|
||||||
|
dT/dt = alpha * d²T/dx²
|
||||||
|
|
||||||
|
Boundary conditions:
|
||||||
|
- x=0 (Neumann): heat flux step Q(t) applied via ghost cell
|
||||||
|
- x=L (Robin): convective boundary condition
|
||||||
|
|
||||||
|
Returns T_matrix of shape (NX, NT).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# Allow importing config from the project root
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
import config
|
||||||
|
|
||||||
|
|
||||||
|
def solve():
|
||||||
|
"""Run the FTCS solver and return (T_matrix, x_vals, t_vals).
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
T_matrix : np.ndarray, shape (NX, NT)
|
||||||
|
Temperature at each spatial node (row) and time step (column).
|
||||||
|
T_matrix[i, n] = T(x_i, t_n).
|
||||||
|
x_vals : np.ndarray, shape (NX,)
|
||||||
|
Spatial node positions from 0 to L.
|
||||||
|
t_vals : np.ndarray, shape (NT,)
|
||||||
|
Time values from 0 to T_END.
|
||||||
|
"""
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# Parameters from config
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
alpha = config.ALPHA
|
||||||
|
k = config.K
|
||||||
|
L = config.L
|
||||||
|
T0 = config.T0
|
||||||
|
Q_val = config.Q_VAL
|
||||||
|
t_step = config.T_STEP
|
||||||
|
h = config.H_CONV
|
||||||
|
T_amb = config.T_AMB
|
||||||
|
T_end = config.T_END
|
||||||
|
NX = config.NX
|
||||||
|
NT = config.NT
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# Grid
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
x_vals = np.linspace(0.0, L, NX)
|
||||||
|
t_vals = np.linspace(0.0, T_end, NT)
|
||||||
|
dx = x_vals[1] - x_vals[0]
|
||||||
|
dt = t_vals[1] - t_vals[0]
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# Stability check (CFL for explicit heat equation: r <= 0.5)
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
r = alpha * dt / dx**2
|
||||||
|
if dt > dx**2 / (2.0 * alpha):
|
||||||
|
print(
|
||||||
|
f"[FDM WARNING] Stability condition violated: "
|
||||||
|
f"dt={dt:.6g} > dx²/(2*alpha)={dx**2/(2.0*alpha):.6g} (r={r:.4f} > 0.5). "
|
||||||
|
"Solution may diverge."
|
||||||
|
)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# Allocate output matrix and set initial condition
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
T_matrix = np.zeros((NX, NT), dtype=np.float64)
|
||||||
|
T_matrix[:, 0] = T0 # uniform IC
|
||||||
|
|
||||||
|
# Working array for the current time level
|
||||||
|
T_cur = np.full(NX, T0, dtype=np.float64)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# Time integration
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
for n in range(NT - 1):
|
||||||
|
t_now = t_vals[n]
|
||||||
|
|
||||||
|
# --- Neumann BC at x=0: ghost cell ---
|
||||||
|
# Q(t) = Q_val if t >= t_step else 0
|
||||||
|
Q = Q_val if t_now >= t_step else 0.0
|
||||||
|
T_ghost = T_cur[1] + 2.0 * dx * Q / k # ghost node T[-1]
|
||||||
|
|
||||||
|
# --- Interior FTCS update (indices 1 .. NX-2) ---
|
||||||
|
T_new = T_cur.copy()
|
||||||
|
T_new[1:-1] = T_cur[1:-1] + r * (T_cur[2:] - 2.0 * T_cur[1:-1] + T_cur[:-2])
|
||||||
|
|
||||||
|
# --- Apply Neumann BC at i=0 using ghost cell ---
|
||||||
|
T_new[0] = T_cur[0] + r * (T_cur[1] - 2.0 * T_cur[0] + T_ghost)
|
||||||
|
|
||||||
|
# --- Apply Robin BC at x=L (explicit) ---
|
||||||
|
T_new[-1] = (T_cur[-2] + dx * h / k * T_amb) / (1.0 + dx * h / k)
|
||||||
|
|
||||||
|
T_cur = T_new
|
||||||
|
T_matrix[:, n + 1] = T_cur
|
||||||
|
|
||||||
|
return T_matrix, x_vals, t_vals
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import numpy as np
|
||||||
|
import plotly.graph_objects as go
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
import config
|
||||||
|
|
||||||
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
FDM_ANIM_DIR = os.path.join(BASE_DIR, 'animations', 'fdm')
|
||||||
|
|
||||||
|
|
||||||
|
def _next_path(base_dir, prefix, ext):
|
||||||
|
i = 1
|
||||||
|
while True:
|
||||||
|
path = os.path.join(base_dir, f'{prefix}_{i:03d}{ext}')
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return path
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
|
||||||
|
def visualize_fdm(T_matrix, x_vals, t_vals):
|
||||||
|
"""Produce three HTML visualisations for the FDM solver output.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
T_matrix : np.ndarray, shape (NX, NT)
|
||||||
|
Temperature field: T_matrix[i, n] = T(x_i, t_n).
|
||||||
|
x_vals : np.ndarray, shape (NX,)
|
||||||
|
Spatial node positions.
|
||||||
|
t_vals : np.ndarray, shape (NT,)
|
||||||
|
Time values.
|
||||||
|
"""
|
||||||
|
os.makedirs(FDM_ANIM_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 1. Heatmap
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
zmax = float(np.max(np.abs(T_matrix - config.T0)))
|
||||||
|
# Use symmetric range centred on T0 if there is any variation,
|
||||||
|
# otherwise fall back to the raw range.
|
||||||
|
z_data = T_matrix.T # shape (NT, NX) — rows=time, cols=space
|
||||||
|
|
||||||
|
if zmax > 0:
|
||||||
|
z_center = float(np.mean(T_matrix))
|
||||||
|
z_abs = float(np.max(np.abs(T_matrix - z_center)))
|
||||||
|
zmin_sym = z_center - z_abs
|
||||||
|
zmax_sym = z_center + z_abs
|
||||||
|
else:
|
||||||
|
zmin_sym = float(np.min(T_matrix))
|
||||||
|
zmax_sym = float(np.max(T_matrix))
|
||||||
|
|
||||||
|
fig_heatmap = go.Figure(go.Heatmap(
|
||||||
|
z=z_data,
|
||||||
|
x=x_vals,
|
||||||
|
y=t_vals,
|
||||||
|
colorscale='RdBu_r',
|
||||||
|
zmin=zmin_sym,
|
||||||
|
zmax=zmax_sym,
|
||||||
|
colorbar=dict(title='T [°C]'),
|
||||||
|
))
|
||||||
|
fig_heatmap.update_layout(
|
||||||
|
title='FDM — Heat Equation T(x,t)',
|
||||||
|
xaxis_title='x [m]',
|
||||||
|
yaxis_title='t [s]',
|
||||||
|
height=500,
|
||||||
|
)
|
||||||
|
|
||||||
|
heatmap_path = _next_path(FDM_ANIM_DIR, 'heatmap', '.html')
|
||||||
|
fig_heatmap.write_html(heatmap_path)
|
||||||
|
print(f"Heatmap saved → {heatmap_path}")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 2. Animated profile T(x) evolving in time
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
L = config.L
|
||||||
|
n_frames = len(t_vals)
|
||||||
|
|
||||||
|
# Subsample frames for a manageable animation (max ~200 frames)
|
||||||
|
max_frames = 200
|
||||||
|
step = max(1, n_frames // max_frames)
|
||||||
|
frame_indices = list(range(0, n_frames, step))
|
||||||
|
|
||||||
|
y_min = float(np.min(T_matrix)) - 1.0
|
||||||
|
y_max = float(np.max(T_matrix)) + 1.0
|
||||||
|
|
||||||
|
vline_shapes = [
|
||||||
|
dict(type='line', x0=0, x1=0, y0=y_min, y1=y_max,
|
||||||
|
line=dict(color='grey', dash='dash', width=1)),
|
||||||
|
dict(type='line', x0=L, x1=L, y0=y_min, y1=y_max,
|
||||||
|
line=dict(color='grey', dash='dash', width=1)),
|
||||||
|
]
|
||||||
|
|
||||||
|
frames = []
|
||||||
|
for idx in frame_indices:
|
||||||
|
frames.append(go.Frame(
|
||||||
|
data=[go.Scatter(
|
||||||
|
x=x_vals,
|
||||||
|
y=T_matrix[:, idx],
|
||||||
|
mode='lines',
|
||||||
|
line=dict(color='royalblue', width=2),
|
||||||
|
name='FDM',
|
||||||
|
)],
|
||||||
|
name=str(idx),
|
||||||
|
layout=go.Layout(
|
||||||
|
title_text=f'FDM | t = {t_vals[idx]:.3f} s',
|
||||||
|
),
|
||||||
|
))
|
||||||
|
|
||||||
|
fig_anim = go.Figure(
|
||||||
|
data=[go.Scatter(
|
||||||
|
x=x_vals,
|
||||||
|
y=T_matrix[:, frame_indices[0]],
|
||||||
|
mode='lines',
|
||||||
|
line=dict(color='royalblue', width=2),
|
||||||
|
name='FDM',
|
||||||
|
)],
|
||||||
|
layout=go.Layout(
|
||||||
|
title=f'FDM | t = {t_vals[frame_indices[0]]:.3f} s',
|
||||||
|
xaxis=dict(title='x [m]', range=[-0.02 * L, 1.02 * L]),
|
||||||
|
yaxis=dict(title='T [°C]', range=[y_min, y_max]),
|
||||||
|
shapes=vline_shapes,
|
||||||
|
updatemenus=[dict(
|
||||||
|
type='buttons',
|
||||||
|
showactive=False,
|
||||||
|
y=1.15,
|
||||||
|
x=0.5,
|
||||||
|
xanchor='center',
|
||||||
|
buttons=[
|
||||||
|
dict(
|
||||||
|
label='▶ Play',
|
||||||
|
method='animate',
|
||||||
|
args=[None, dict(
|
||||||
|
frame=dict(duration=40, redraw=False),
|
||||||
|
fromcurrent=True,
|
||||||
|
mode='immediate',
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
dict(
|
||||||
|
label='⏸ Pause',
|
||||||
|
method='animate',
|
||||||
|
args=[[None], dict(
|
||||||
|
frame=dict(duration=0, redraw=False),
|
||||||
|
mode='immediate',
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)],
|
||||||
|
sliders=[dict(
|
||||||
|
steps=[
|
||||||
|
dict(
|
||||||
|
method='animate',
|
||||||
|
args=[[str(idx)], dict(
|
||||||
|
mode='immediate',
|
||||||
|
frame=dict(duration=0, redraw=False),
|
||||||
|
)],
|
||||||
|
label=f'{t_vals[idx]:.2f}',
|
||||||
|
)
|
||||||
|
for idx in frame_indices
|
||||||
|
],
|
||||||
|
transition=dict(duration=0),
|
||||||
|
x=0.05,
|
||||||
|
y=0,
|
||||||
|
len=0.9,
|
||||||
|
currentvalue=dict(prefix='t = ', font=dict(size=14)),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
frames=frames,
|
||||||
|
)
|
||||||
|
|
||||||
|
anim_path = _next_path(FDM_ANIM_DIR, 'animation', '.html')
|
||||||
|
fig_anim.write_html(anim_path)
|
||||||
|
print(f"Animation saved → {anim_path}")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 3. Time evolution at fixed spatial points
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
fixed_fractions = [0.0, 0.25, 0.5, 0.75, 1.0]
|
||||||
|
colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd']
|
||||||
|
|
||||||
|
fig_ts = go.Figure()
|
||||||
|
|
||||||
|
for frac, color in zip(fixed_fractions, colors):
|
||||||
|
x_target = frac * L
|
||||||
|
idx_x = int(np.argmin(np.abs(x_vals - x_target)))
|
||||||
|
label = f'x = {x_vals[idx_x]:.2f} m'
|
||||||
|
fig_ts.add_trace(go.Scatter(
|
||||||
|
x=t_vals,
|
||||||
|
y=T_matrix[idx_x, :],
|
||||||
|
mode='lines',
|
||||||
|
name=label,
|
||||||
|
line=dict(color=color, width=2),
|
||||||
|
))
|
||||||
|
|
||||||
|
# "Heat ON" vertical dashed line at t = T_STEP
|
||||||
|
t_step = config.T_STEP
|
||||||
|
t_min_val = float(np.min(t_vals))
|
||||||
|
t_max_val = float(np.max(t_vals))
|
||||||
|
T_min_val = float(np.min(T_matrix)) - 1.0
|
||||||
|
T_max_val = float(np.max(T_matrix)) + 1.0
|
||||||
|
|
||||||
|
fig_ts.add_shape(
|
||||||
|
type='line',
|
||||||
|
x0=t_step, x1=t_step,
|
||||||
|
y0=T_min_val, y1=T_max_val,
|
||||||
|
line=dict(color='red', dash='dash', width=1.5),
|
||||||
|
)
|
||||||
|
fig_ts.add_annotation(
|
||||||
|
x=t_step,
|
||||||
|
y=T_max_val,
|
||||||
|
text='Heat ON',
|
||||||
|
showarrow=False,
|
||||||
|
yanchor='top',
|
||||||
|
font=dict(color='red', size=11),
|
||||||
|
)
|
||||||
|
|
||||||
|
fig_ts.update_layout(
|
||||||
|
title='FDM — Temperature evolution at fixed points',
|
||||||
|
xaxis=dict(title='t [s]', range=[t_min_val, t_max_val]),
|
||||||
|
yaxis=dict(title='T [°C]', range=[T_min_val, T_max_val]),
|
||||||
|
legend=dict(x=0.01, y=0.99),
|
||||||
|
height=480,
|
||||||
|
)
|
||||||
|
|
||||||
|
ts_path = _next_path(FDM_ANIM_DIR, 'time_series', '.html')
|
||||||
|
fig_ts.write_html(ts_path)
|
||||||
|
print(f"Time-series saved → {ts_path}")
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import torch
|
||||||
|
import torch.nn as nn
|
||||||
|
import config
|
||||||
|
|
||||||
|
|
||||||
|
class HeatPINN(nn.Module):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.net = nn.Sequential(
|
||||||
|
nn.Linear(2, 128), nn.Tanh(),
|
||||||
|
nn.Linear(128, 128), nn.Tanh(),
|
||||||
|
nn.Linear(128, 128), nn.Tanh(),
|
||||||
|
nn.Linear(128, 128), nn.Tanh(),
|
||||||
|
nn.Linear(128, 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
def forward(self, x):
|
||||||
|
# Output scaled to physical range: T_AMB + (Q*L/K) * net
|
||||||
|
# net learns dimensionless perturbation in [0,1] range
|
||||||
|
T_scale = config.T_AMB + (config.Q_VAL * config.L / config.K) * self.net(x)
|
||||||
|
return T_scale
|
||||||
|
|
||||||
|
|
||||||
|
def heat_pinn_loss(model, x_f, t_f, x_ic, t_bc, w_pde=1.0, w_ic=1.0, w_bc=10.0):
|
||||||
|
# Characteristic scales for normalization
|
||||||
|
T_char = config.Q_VAL * config.L / config.K # ~50 °C — temperature scale
|
||||||
|
grad_char = (config.Q_VAL / config.K) ** 2 # ~2500 — gradient scale²
|
||||||
|
|
||||||
|
# PDE residual: dT/dt - alpha * d2T/dx2 = 0 (normalized by T_char/t_char)
|
||||||
|
x_f = x_f.detach().requires_grad_(True)
|
||||||
|
t_f = t_f.detach().requires_grad_(True)
|
||||||
|
T_f = model(torch.stack([x_f, t_f], dim=1))
|
||||||
|
dT_dt = torch.autograd.grad(T_f.sum(), t_f, create_graph=True)[0]
|
||||||
|
dT_dx = torch.autograd.grad(T_f.sum(), x_f, create_graph=True)[0]
|
||||||
|
d2T_dx2 = torch.autograd.grad(dT_dx.sum(), x_f, create_graph=True)[0]
|
||||||
|
pde_scale = (T_char / config.T_END) ** 2 + 1e-8
|
||||||
|
L_pde = ((dT_dt - config.ALPHA * d2T_dx2) ** 2).mean() / pde_scale
|
||||||
|
|
||||||
|
# IC: T(x, 0) = T0 — normalized by T_char²
|
||||||
|
T_ic_pred = model(torch.stack([x_ic, torch.zeros_like(x_ic)], dim=1))
|
||||||
|
T_ic_true = torch.full_like(T_ic_pred, config.T0)
|
||||||
|
L_ic = ((T_ic_pred - T_ic_true) ** 2).mean() / (T_char ** 2 + 1e-8)
|
||||||
|
|
||||||
|
# BC x=0: Neumann — dT/dx + Q(t)/K = 0
|
||||||
|
x_left = torch.zeros(t_bc.shape[0], device=t_bc.device).requires_grad_(True)
|
||||||
|
T_left = model(torch.stack([x_left, t_bc.detach()], dim=1))
|
||||||
|
dT_dx_left = torch.autograd.grad(T_left.sum(), x_left, create_graph=True)[0]
|
||||||
|
Q_t = torch.where(t_bc >= config.T_STEP,
|
||||||
|
torch.tensor(config.Q_VAL, device=t_bc.device, dtype=t_bc.dtype),
|
||||||
|
torch.tensor(0.0, device=t_bc.device, dtype=t_bc.dtype))
|
||||||
|
L_bc_left = ((dT_dx_left + Q_t / config.K) ** 2).mean() / grad_char
|
||||||
|
|
||||||
|
# BC x=L: Robin — dT/dx + H_CONV/K * (T(L,t) - T_AMB) = 0
|
||||||
|
x_right = torch.full((t_bc.shape[0],), config.L, device=t_bc.device).requires_grad_(True)
|
||||||
|
T_right = model(torch.stack([x_right, t_bc.detach()], dim=1))
|
||||||
|
dT_dx_right = torch.autograd.grad(T_right.sum(), x_right, create_graph=True)[0]
|
||||||
|
L_bc_right = ((dT_dx_right + (config.H_CONV / config.K) * (T_right.squeeze() - config.T_AMB)) ** 2).mean() / grad_char
|
||||||
|
|
||||||
|
L_bc = L_bc_left + L_bc_right
|
||||||
|
total = w_pde * L_pde + w_ic * L_ic + w_bc * L_bc
|
||||||
|
return total, L_pde, L_ic, L_bc
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
torch>=2.0.0
|
||||||
|
pandas>=2.0.0
|
||||||
|
numpy>=1.24.0
|
||||||
|
scikit-learn>=1.3.0
|
||||||
|
plotly>=5.15.0
|
||||||
+153
@@ -0,0 +1,153 @@
|
|||||||
|
import os
|
||||||
|
import numpy as np
|
||||||
|
import plotly.graph_objects as go
|
||||||
|
from plotly.subplots import make_subplots
|
||||||
|
import config
|
||||||
|
|
||||||
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
ANIMATIONS_DIR = os.path.join(BASE_DIR, 'animations')
|
||||||
|
|
||||||
|
|
||||||
|
def visualize_heat_field(T_pred, x_vals, t_vals, T_fdm):
|
||||||
|
os.makedirs(ANIMATIONS_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# Downsample T_fdm from shape (NX, NT) to (NX, len(t_vals))
|
||||||
|
nt_pred = len(t_vals)
|
||||||
|
t_indices = np.linspace(0, T_fdm.shape[1] - 1, nt_pred, dtype=int)
|
||||||
|
T_fdm_ds = T_fdm[:, t_indices] # now shape (NX, nt_pred)
|
||||||
|
|
||||||
|
# --- Static heatmap: PINN vs FDM ---
|
||||||
|
fig_map = make_subplots(
|
||||||
|
rows=1, cols=2,
|
||||||
|
subplot_titles=["PINN Prediction T(x,t)", "FDM Reference T(x,t)"],
|
||||||
|
shared_yaxes=True,
|
||||||
|
)
|
||||||
|
colorscale = 'Hot_r'
|
||||||
|
zmin = float(min(np.min(T_pred), np.min(T_fdm_ds)))
|
||||||
|
zmax = float(max(np.max(T_pred), np.max(T_fdm_ds)))
|
||||||
|
|
||||||
|
fig_map.add_trace(go.Heatmap(
|
||||||
|
z=T_pred.T, x=x_vals, y=t_vals,
|
||||||
|
colorscale=colorscale, zmin=zmin, zmax=zmax,
|
||||||
|
colorbar=dict(x=0.46, title='T [°C]'),
|
||||||
|
), row=1, col=1)
|
||||||
|
|
||||||
|
fig_map.add_trace(go.Heatmap(
|
||||||
|
z=T_fdm_ds.T, x=x_vals, y=t_vals,
|
||||||
|
colorscale=colorscale, zmin=zmin, zmax=zmax,
|
||||||
|
colorbar=dict(x=1.01, title='T [°C]'),
|
||||||
|
), row=1, col=2)
|
||||||
|
|
||||||
|
fig_map.update_xaxes(title_text='x')
|
||||||
|
fig_map.update_yaxes(title_text='t', row=1, col=1)
|
||||||
|
fig_map.update_layout(title_text='Heat Equation PINN vs FDM', height=450)
|
||||||
|
|
||||||
|
map_path = _next_path('heatmap', '.html')
|
||||||
|
fig_map.write_html(map_path)
|
||||||
|
print(f"Heatmap saved → {map_path}")
|
||||||
|
|
||||||
|
# --- Animated profile T(x) evolving in time ---
|
||||||
|
n_frames = len(t_vals)
|
||||||
|
frames = []
|
||||||
|
for i in range(n_frames):
|
||||||
|
frames.append(go.Frame(
|
||||||
|
data=[
|
||||||
|
go.Scatter(x=x_vals, y=T_pred[:, i], mode='lines',
|
||||||
|
line=dict(color='royalblue', width=2), name='PINN'),
|
||||||
|
go.Scatter(x=x_vals, y=T_fdm_ds[:, i], mode='lines',
|
||||||
|
line=dict(color='firebrick', width=2, dash='dash'), name='FDM'),
|
||||||
|
],
|
||||||
|
name=str(i),
|
||||||
|
layout=go.Layout(title_text=f'Heat Equation PINN vs FDM | t = {t_vals[i]:.3f}'),
|
||||||
|
))
|
||||||
|
|
||||||
|
fig_anim = go.Figure(
|
||||||
|
data=frames[0].data,
|
||||||
|
layout=go.Layout(
|
||||||
|
title=f'Heat Equation PINN vs FDM | t = {t_vals[0]:.3f}',
|
||||||
|
xaxis=dict(title='x [m]', range=[-0.02, config.L + 0.02]),
|
||||||
|
yaxis=dict(title='T [°C]', range=[zmin - 1, zmax + 1]),
|
||||||
|
legend=dict(x=0.75, y=0.95),
|
||||||
|
updatemenus=[dict(
|
||||||
|
type='buttons', showactive=False, y=1.15, x=0.5, xanchor='center',
|
||||||
|
buttons=[
|
||||||
|
dict(label='▶ Play', method='animate',
|
||||||
|
args=[None, dict(frame=dict(duration=40, redraw=False),
|
||||||
|
fromcurrent=True, mode='immediate')]),
|
||||||
|
dict(label='⏸ Pause', method='animate',
|
||||||
|
args=[[None], dict(frame=dict(duration=0, redraw=False),
|
||||||
|
mode='immediate')]),
|
||||||
|
],
|
||||||
|
)],
|
||||||
|
sliders=[dict(
|
||||||
|
steps=[dict(method='animate', args=[[str(i)],
|
||||||
|
dict(mode='immediate', frame=dict(duration=0, redraw=False))],
|
||||||
|
label=f'{t_vals[i]:.2f}') for i in range(n_frames)],
|
||||||
|
transition=dict(duration=0), x=0.05, y=0, len=0.9,
|
||||||
|
currentvalue=dict(prefix='t = ', font=dict(size=14)),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
frames=frames,
|
||||||
|
)
|
||||||
|
|
||||||
|
anim_path = _next_path('heat_animation', '.html')
|
||||||
|
fig_anim.write_html(anim_path)
|
||||||
|
print(f"Animation saved → {anim_path}")
|
||||||
|
|
||||||
|
# --- Time-series comparison at fixed spatial points ---
|
||||||
|
# Spatial indices for x=0, x=L/2, x=L
|
||||||
|
nx = len(x_vals)
|
||||||
|
idx_x0 = 0
|
||||||
|
idx_xmid = nx // 2
|
||||||
|
idx_xL = nx - 1
|
||||||
|
|
||||||
|
points = [
|
||||||
|
(idx_x0, 'x=0', 'blue'),
|
||||||
|
(idx_xmid, 'x=L/2', 'green'),
|
||||||
|
(idx_xL, 'x=L', 'red'),
|
||||||
|
]
|
||||||
|
|
||||||
|
fig_ts = go.Figure()
|
||||||
|
|
||||||
|
for idx, label, color in points:
|
||||||
|
fig_ts.add_trace(go.Scatter(
|
||||||
|
x=t_vals, y=T_pred[idx, :],
|
||||||
|
mode='lines',
|
||||||
|
line=dict(color=color, width=2, dash='solid'),
|
||||||
|
name=f'PINN {label}',
|
||||||
|
))
|
||||||
|
fig_ts.add_trace(go.Scatter(
|
||||||
|
x=t_vals, y=T_fdm_ds[idx, :],
|
||||||
|
mode='lines',
|
||||||
|
line=dict(color=color, width=2, dash='dash'),
|
||||||
|
name=f'FDM {label}',
|
||||||
|
))
|
||||||
|
|
||||||
|
# Vertical dashed line at T_STEP ("Heat ON")
|
||||||
|
fig_ts.add_vline(
|
||||||
|
x=config.T_STEP,
|
||||||
|
line=dict(color='red', dash='dash', width=1.5),
|
||||||
|
annotation_text='Heat ON',
|
||||||
|
annotation_position='top right',
|
||||||
|
)
|
||||||
|
|
||||||
|
fig_ts.update_layout(
|
||||||
|
title='Heat Equation PINN vs FDM — Time Series at Fixed Points',
|
||||||
|
xaxis_title='t',
|
||||||
|
yaxis_title='T(x,t)',
|
||||||
|
legend=dict(x=0.01, y=0.99),
|
||||||
|
height=500,
|
||||||
|
)
|
||||||
|
|
||||||
|
comparison_path = _next_path('comparison', '.html')
|
||||||
|
fig_ts.write_html(comparison_path)
|
||||||
|
print(f"Time-series saved → {comparison_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def _next_path(prefix, ext):
|
||||||
|
i = 1
|
||||||
|
while True:
|
||||||
|
path = os.path.join(ANIMATIONS_DIR, f'{prefix}_{i:03d}{ext}')
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return path
|
||||||
|
i += 1
|
||||||
Reference in New Issue
Block a user