itstheraj commited on
Commit
cdcc0fd
·
0 Parent(s):

initial commit

Browse files
Files changed (17) hide show
  1. .gitattributes +1 -0
  2. .gitignore +6 -0
  3. LICENSE +474 -0
  4. README.md +218 -0
  5. config.json +82 -0
  6. configuration_pde.py +47 -0
  7. example.py +28 -0
  8. input_builder.py +117 -0
  9. model.safetensors +3 -0
  10. modeling.py +132 -0
  11. modeling_pde.py +61 -0
  12. pack.csv +2 -0
  13. pack.json +13 -0
  14. streaming/manifest.json +14 -0
  15. streaming/model_fp16.safetensors +3 -0
  16. train.py +417 -0
  17. verify.py +44 -0
.gitattributes ADDED
@@ -0,0 +1 @@
 
 
1
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ .ruff_cache/
2
+ .venv/
3
+ __pycache__/
4
+ *.swp
5
+ *.kate-swp
6
+ *~
LICENSE ADDED
@@ -0,0 +1,474 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Lowdown Labs Lovely License 1.0 (LLLL-1.0)
2
+
3
+ Everything in this repository (the model weights, the configuration, and the code) is
4
+ released by Lowdown Labs under two licenses that apply at the same time. To use this work
5
+ you must comply with BOTH of them. Where a term in one is stricter than the other, the
6
+ stricter term controls. "Lowdown Labs Lovely License 1.0" is a convenience name for this
7
+ exact pair; it is not a new legal instrument.
8
+
9
+ SPDX-License-Identifier: CC-BY-NC-4.0 AND LicenseRef-Hippocratic-3.0
10
+
11
+ Commercial licensing. The grant below is non-commercial only. Commercial licenses are
12
+ sold separately by Lowdown Labs on a per-customer basis. To use this work, its weights, or
13
+ its outputs for any commercial purpose, contact Lowdown Labs to purchase a commercial
14
+ license. A commercial license does not remove the Hippocratic ethical-use obligations in
15
+ Part 2; those apply to commercial licensees as well.
16
+
17
+ ==============================================================================
18
+ Part 1 of 2. Creative Commons Attribution-NonCommercial 4.0 International (CC BY-NC 4.0)
19
+ ==============================================================================
20
+
21
+ You may share and adapt this work for non-commercial purposes, with attribution to
22
+ Lowdown Labs. Commercial use is not granted under this license.
23
+
24
+ Full legal text: https://creativecommons.org/licenses/by-nc/4.0/legalcode
25
+ Plain-language summary: https://creativecommons.org/licenses/by-nc/4.0/
26
+ SPDX-License-Identifier: CC-BY-NC-4.0
27
+
28
+ ==============================================================================
29
+ Part 2 of 2. The Hippocratic License 3.0 (ethical use)
30
+ ==============================================================================
31
+
32
+ Module set enabled: bds, cl, eco, extr, ffd, law, media, mil, my, soc, sup, sv, usta.
33
+ Canonical build: https://firstdonoharm.dev/build/?modules=bds,cl,eco,extr,ffd,law,media,mil,my,soc,sup,sv,usta
34
+
35
+ The verbatim official Hippocratic License 3.0 text for exactly this module set follows,
36
+ between the markers.
37
+
38
+ --------------------- BEGIN OFFICIAL HIPPOCRATIC LICENSE 3.0 TEXT ---------------------
39
+
40
+ HIPPOCRATIC LICENSE
41
+
42
+ Version 3.0, October 2021
43
+
44
+ https://firstdonoharm.dev/version/3/0/bds-cl-eco-extr-ffd-law-media-mil-my-soc-sup-sv-usta.txt
45
+
46
+ TERMS AND CONDITIONS
47
+
48
+ TERMS AND CONDITIONS FOR USE, COPY, MODIFICATION, PREPARATION OF DERIVATIVE
49
+ WORK, REPRODUCTION, AND DISTRIBUTION:
50
+
51
+ 1. DEFINITIONS:
52
+
53
+ This section defines certain terms used throughout this license agreement.
54
+
55
+ 1.1. “License” means the terms and conditions, as stated herein, for use, copy,
56
+ modification, preparation of derivative work, reproduction, and distribution of
57
+ Software (as defined below).
58
+
59
+ 1.2. “Licensor” means the copyright and/or patent owner or entity authorized by
60
+ the copyright and/or patent owner that is granting the License.
61
+
62
+ 1.3. “Licensee” means the individual or entity exercising permissions granted by
63
+ this License, including the use, copy, modification, preparation of derivative
64
+ work, reproduction, and distribution of Software (as defined below).
65
+
66
+ 1.4. “Software” means any copyrighted work, including but not limited to
67
+ software code, authored by Licensor and made available under this License.
68
+
69
+ 1.5. “Supply Chain” means the sequence of processes involved in the production
70
+ and/or distribution of a commodity, good, or service offered by the Licensee.
71
+
72
+ 1.6. “Supply Chain Impacted Party” or “Supply Chain Impacted Parties” means any
73
+ person(s) directly impacted by any of Licensee’s Supply Chain, including the
74
+ practices of all persons or entities within the Supply Chain prior to a good or
75
+ service reaching the Licensee.
76
+
77
+ 1.7. “Duty of Care” is defined by its use in tort law, delict law, and/or
78
+ similar bodies of law closely related to tort and/or delict law, including
79
+ without limitation, a requirement to act with the watchfulness, attention,
80
+ caution, and prudence that a reasonable person in the same or similar
81
+ circumstances would use towards any Supply Chain Impacted Party.
82
+
83
+ 1.8. “Worker” is defined to include any and all permanent, temporary, and agency
84
+ workers, as well as piece-rate, salaried, hourly paid, legal young (minors),
85
+ part-time, night, and migrant workers.
86
+
87
+ 2. INTELLECTUAL PROPERTY GRANTS:
88
+
89
+ This section identifies intellectual property rights granted to a Licensee.
90
+
91
+ 2.1. Grant of Copyright License: Subject to the terms and conditions of this
92
+ License, Licensor hereby grants to Licensee a worldwide, non-exclusive,
93
+ no-charge, royalty-free copyright license to use, copy, modify, prepare
94
+ derivative work, reproduce, or distribute the Software, Licensor authored
95
+ modified software, or other work derived from the Software.
96
+
97
+ 2.2. Grant of Patent License: Subject to the terms and conditions of this
98
+ License, Licensor hereby grants Licensee a worldwide, non-exclusive, no-charge,
99
+ royalty-free patent license to make, have made, use, offer to sell, sell,
100
+ import, and otherwise transfer Software.
101
+
102
+ 3. ETHICAL STANDARDS:
103
+
104
+ This section lists conditions the Licensee must comply with in order to have
105
+ rights under this License.
106
+
107
+ The rights granted to the Licensee by this License are expressly made subject to
108
+ the Licensee’s ongoing compliance with the following conditions:
109
+
110
+ * 3.1. The Licensee SHALL NOT, whether directly or indirectly, through agents
111
+ or assigns:
112
+
113
+ * 3.1.1. Infringe upon any person’s right to life or security of person,
114
+ engage in extrajudicial killings, or commit murder, without lawful cause
115
+ (See Article 3, United Nations Universal Declaration of Human Rights;
116
+ Article 6, International Covenant on Civil and Political Rights)
117
+
118
+ * 3.1.2. Hold any person in slavery, servitude, or forced labor (See Article
119
+ 4, United Nations Universal Declaration of Human Rights; Article 8,
120
+ International Covenant on Civil and Political Rights);
121
+
122
+ * 3.1.3. Contribute to the institution of slavery, slave trading, forced
123
+ labor, or unlawful child labor (See Article 4, United Nations Universal
124
+ Declaration of Human Rights; Article 8, International Covenant on Civil and
125
+ Political Rights);
126
+
127
+ * 3.1.4. Torture or subject any person to cruel, inhumane, or degrading
128
+ treatment or punishment (See Article 5, United Nations Universal
129
+ Declaration of Human Rights; Article 7, International Covenant on Civil and
130
+ Political Rights);
131
+
132
+ * 3.1.5. Discriminate on the basis of sex, gender, sexual orientation, race,
133
+ ethnicity, nationality, religion, caste, age, medical disability or
134
+ impairment, and/or any other like circumstances (See Article 7, United
135
+ Nations Universal Declaration of Human Rights; Article 2, International
136
+ Covenant on Economic, Social and Cultural Rights; Article 26, International
137
+ Covenant on Civil and Political Rights);
138
+
139
+ * 3.1.6. Prevent any person from exercising his/her/their right to seek an
140
+ effective remedy by a competent court or national tribunal (including
141
+ domestic judicial systems, international courts, arbitration bodies, and
142
+ other adjudicating bodies) for actions violating the fundamental rights
143
+ granted to him/her/them by applicable constitutions, applicable laws, or by
144
+ this License (See Article 8, United Nations Universal Declaration of Human
145
+ Rights; Articles 9 and 14, International Covenant on Civil and Political
146
+ Rights);
147
+
148
+ * 3.1.7. Subject any person to arbitrary arrest, detention, or exile (See
149
+ Article 9, United Nations Universal Declaration of Human Rights; Article 9,
150
+ International Covenant on Civil and Political Rights);
151
+
152
+ * 3.1.8. Subject any person to arbitrary interference with a person’s
153
+ privacy, family, home, or correspondence without the express written
154
+ consent of the person (See Article 12, United Nations Universal Declaration
155
+ of Human Rights; Article 17, International Covenant on Civil and Political
156
+ Rights);
157
+
158
+ * 3.1.9. Arbitrarily deprive any person of his/her/their property (See
159
+ Article 17, United Nations Universal Declaration of Human Rights);
160
+
161
+ * 3.1.10. Forcibly remove indigenous peoples from their lands or territories
162
+ or take any action with the aim or effect of dispossessing indigenous
163
+ peoples from their lands, territories, or resources, including without
164
+ limitation the intellectual property or traditional knowledge of indigenous
165
+ peoples, without the free, prior, and informed consent of indigenous
166
+ peoples concerned (See Articles 8 and 10, United Nations Declaration on the
167
+ Rights of Indigenous Peoples);
168
+ * 3.1.11. Fossil Fuel Divestment: Be an individual or entity, or a
169
+ representative, agent, affiliate, successor, attorney, or assign of an
170
+ individual or entity, on the FFI Solutions Carbon Underground 200 list
171
+ [https://www.ffisolutions.com/research-analytics-index-solutions/research-screening/the-carbon-underground-200/?cn-reloaded=1];
172
+
173
+ * 3.1.12. Ecocide: Commit ecocide:
174
+
175
+ * 3.1.12.1. For the purpose of this section, “ecocide” means unlawful or
176
+ wanton acts committed with knowledge that there is a substantial
177
+ likelihood of severe and either widespread or long-term damage to the
178
+ environment being caused by those acts;
179
+
180
+ * 3.1.12.2. For the purpose of further defining ecocide and the terms
181
+ contained in the previous paragraph:
182
+
183
+ * 3.1.12.2.1. “Wanton” means with reckless disregard for damage which
184
+ would be clearly excessive in relation to the social and economic
185
+ benefits anticipated;
186
+
187
+ * 3.1.12.2.2. “Severe” means damage which involves very serious adverse
188
+ changes, disruption, or harm to any element of the environment,
189
+ including grave impacts on human life or natural, cultural, or
190
+ economic resources;
191
+
192
+ * 3.1.12.2.3. “Widespread” means damage which extends beyond a limited
193
+ geographic area, crosses state boundaries, or is suffered by an entire
194
+ ecosystem or species or a large number of human beings;
195
+
196
+ * 3.1.12.2.4. “Long-term” means damage which is irreversible or which
197
+ cannot be redressed through natural recovery within a reasonable
198
+ period of time; and
199
+
200
+ * 3.1.12.2.5. “Environment” means the earth, its biosphere, cryosphere,
201
+ lithosphere, hydrosphere, and atmosphere, as well as outer space
202
+
203
+ (See Section II, Independent Expert Panel for the Legal Definition of
204
+ Ecocide, Stop Ecocide Foundation and the Promise Institute for Human
205
+ Rights at UCLA School of Law, June 2021);
206
+
207
+ * 3.1.13. Extractive Industries: Be an individual or entity, or a
208
+ representative, agent, affiliate, successor, attorney, or assign of an
209
+ individual or entity, that engages in fossil fuel or mineral exploration,
210
+ extraction, development, or sale;
211
+
212
+ * 3.1.14. Boycott / Divestment / Sanctions: Be an individual or entity, or a
213
+ representative, agent, affiliate, successor, attorney, or assign of an
214
+ individual or entity, identified by the Boycott, Divestment, Sanctions
215
+ (“BDS”) movement on its website (https://bdsmovement.net/
216
+ [https://bdsmovement.net/] and
217
+ https://bdsmovement.net/get-involved/what-to-boycott
218
+ [https://bdsmovement.net/get-involved/what-to-boycott]) as a target for
219
+ boycott;
220
+
221
+ * 3.1.15. Myanmar: Be an individual or entity that:
222
+
223
+ * 3.1.15.1. engages in any commercial transactions with the
224
+ Myanmar/Burmese military junta; or
225
+
226
+ * 3.1.15.2. is a representative, agent, affiliate, successor, attorney, or
227
+ assign of the Myanmar/Burmese government;
228
+
229
+ * 3.1.16. US Tariff Act: Be an individual or entity:
230
+
231
+ * 3.1.16.1. which U.S. Customs and Border Protection (CBP) has currently
232
+ issued a Withhold Release Order (WRO) or finding against based on
233
+ reasonable suspicion of forced labor; or
234
+
235
+ * 3.1.16.2. that is a representative, agent, affiliate, successor,
236
+ attorney, or assign of an individual or entity that does business with
237
+ an individual or entity which currently has a WRO or finding from CBP
238
+ issued against it based on reasonable suspicion of forced labor;
239
+
240
+ * 3.1.17. Mass Surveillance: Be a government agency or multinational
241
+ corporation, or a representative, agent, affiliate, successor, attorney,
242
+ or assign of a government or multinational corporation, which participates
243
+ in mass surveillance programs;
244
+
245
+ * 3.1.18. Military Activities: Be an entity or a representative, agent,
246
+ affiliate, successor, attorney, or assign of an entity which conducts
247
+ military activities;
248
+
249
+ * 3.1.19. Law Enforcement: Be an individual or entity, or a representative,
250
+ agent, affiliate, successor, attorney, or assign of an individual or
251
+ entity, that provides good or services to, or otherwise enters into any
252
+ commercial contracts with, any local, state, or federal law enforcement
253
+ agency;
254
+
255
+ * 3.1.20. Media: Be an individual or entity, or a representative, agent,
256
+ affiliate, successor, attorney, or assign of an individual or entity, that
257
+ broadcasts messages promoting killing, torture, or other forms of extreme
258
+ violence;
259
+
260
+ * 3.1.21. Interfere with Workers’ free exercise of the right to organize and
261
+ associate (See Article 20, United Nations Universal Declaration of Human
262
+ Rights; C087 - Freedom of Association and Protection of the Right to
263
+ Organise Convention, 1948 (No. 87), International Labour Organization;
264
+ Article 8, International Covenant on Economic, Social and Cultural Rights);
265
+ and
266
+
267
+ * 3.1.22. Harm the environment in a manner inconsistent with local, state,
268
+ national, or international law.
269
+
270
+ * 3.2. The Licensee SHALL:
271
+
272
+ * 3.2.1. Social Auditing: Only use social auditing mechanisms that adhere to
273
+ Worker-Driven Social Responsibility Network’s Statement of Principles
274
+ (https://wsr-network.org/what-is-wsr/statement-of-principles/
275
+ [https://wsr-network.org/what-is-wsr/statement-of-principles/]) over
276
+ traditional social auditing mechanisms, to the extent the Licensee uses
277
+ any social auditing mechanisms at all;
278
+
279
+ * 3.2.2. Supply Chain: Provide clear, accessible supply chain data to the
280
+ public in accordance with the following conditions:
281
+
282
+ * 3.2.2.1. All data will be on Licensee’s website and/or, to the extent
283
+ Licensee is a representative, agent, affiliate, successor, attorney,
284
+ subsidiary, or assign, on Licensee’s principal’s or parent’s website or
285
+ some other online platform accessible to the public via an internet
286
+ search on a common internet search engine; and
287
+
288
+ * 3.2.2.2. Data published will include, where applicable, manufacturers,
289
+ top tier suppliers, subcontractors, cooperatives, component parts
290
+ producers, and farms;
291
+
292
+ * 3.2.3. Provide equal pay for equal work where the performance of such work
293
+ requires equal skill, effort, and responsibility, and which are performed
294
+ under similar working conditions, except where such payment is made
295
+ pursuant to:
296
+
297
+ * 3.2.3.1. A seniority system;
298
+
299
+ * 3.2.3.2. A merit system;
300
+
301
+ * 3.2.3.3. A system which measures earnings by quantity or quality of
302
+ production; or
303
+
304
+ * 3.2.3.4. A differential based on any other factor other than sex, gender,
305
+ sexual orientation, race, ethnicity, nationality, religion, caste, age,
306
+ medical disability or impairment, and/or any other like circumstances
307
+ (See 29 U.S.C.A. § 206(d)(1); Article 23, United Nations Universal
308
+ Declaration of Human Rights; Article 7, International Covenant on
309
+ Economic, Social and Cultural Rights; Article 26, International Covenant
310
+ on Civil and Political Rights); and
311
+
312
+ * 3.2.4. Allow for reasonable limitation of working hours and periodic
313
+ holidays with pay (See Article 24, United Nations Universal Declaration of
314
+ Human Rights; Article 7, International Covenant on Economic, Social and
315
+ Cultural Rights).
316
+
317
+ 4. SUPPLY CHAIN IMPACTED PARTIES:
318
+
319
+ This section identifies additional individuals or entities that a Licensee could
320
+ harm as a result of violating the Ethical Standards section, the condition that
321
+ the Licensee must voluntarily accept a Duty of Care for those individuals or
322
+ entities, and the right to a private right of action that those individuals or
323
+ entities possess as a result of violations of the Ethical Standards section.
324
+
325
+ 4.1. In addition to the above Ethical Standards, Licensee voluntarily accepts a
326
+ Duty of Care for Supply Chain Impacted Parties of this License, including
327
+ individuals and communities impacted by violations of the Ethical Standards. The
328
+ Duty of Care is breached when a provision within the Ethical Standards section
329
+ is violated by a Licensee, one of its successors or assigns, or by an individual
330
+ or entity that exists within the Supply Chain prior to a good or service
331
+ reaching the Licensee.
332
+
333
+ 4.2. Breaches of the Duty of Care, as stated within this section, shall create a
334
+ private right of action, allowing any Supply Chain Impacted Party harmed by the
335
+ Licensee to take legal action against the Licensee in accordance with applicable
336
+ negligence laws, whether they be in tort law, delict law, and/or similar bodies
337
+ of law closely related to tort and/or delict law, regardless if Licensee is
338
+ directly responsible for the harms suffered by a Supply Chain Impacted Party.
339
+ Nothing in this section shall be interpreted to include acts committed by
340
+ individuals outside of the scope of his/her/their employment.
341
+
342
+ 5. NOTICE: This section explains when a Licensee must notify others of the
343
+ License.
344
+
345
+ 5.1. Distribution of Notice: Licensee must ensure that everyone who receives a
346
+ copy of or uses any part of Software from Licensee, with or without changes,
347
+ also receives the License and the copyright notice included with Software (and
348
+ if included by the Licensor, patent, trademark, and attribution notice).
349
+ Licensee must ensure that License is prominently displayed so that any
350
+ individual or entity seeking to download, copy, use, or otherwise receive any
351
+ part of Software from Licensee is notified of this License and its terms and
352
+ conditions. Licensee must cause any modified versions of the Software to carry
353
+ prominent notices stating that Licensee changed the Software.
354
+
355
+ 5.2. Modified Software: Licensee is free to create modifications of the Software
356
+ and distribute only the modified portion created by Licensee, however, any
357
+ derivative work stemming from the Software or its code must be distributed
358
+ pursuant to this License, including this Notice provision.
359
+
360
+ 5.3. Recipients as Licensees: Any individual or entity that uses, copies,
361
+ modifies, reproduces, distributes, or prepares derivative work based upon the
362
+ Software, all or part of the Software’s code, or a derivative work developed by
363
+ using the Software, including a portion of its code, is a Licensee as defined
364
+ above and is subject to the terms and conditions of this License.
365
+
366
+ 6. REPRESENTATIONS AND WARRANTIES:
367
+
368
+ 6.1. Disclaimer of Warranty: TO THE FULL EXTENT ALLOWED BY LAW, THIS SOFTWARE
369
+ COMES “AS IS,” WITHOUT ANY WARRANTY, EXPRESS OR IMPLIED, AND LICENSOR SHALL NOT
370
+ BE LIABLE TO ANY PERSON OR ENTITY FOR ANY DAMAGES OR OTHER LIABILITY ARISING
371
+ FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THIS LICENSE, UNDER ANY
372
+ LEGAL CLAIM.
373
+
374
+ 6.2. Limitation of Liability: LICENSEE SHALL HOLD LICENSOR HARMLESS AGAINST ANY
375
+ AND ALL CLAIMS, DEBTS, DUES, LIABILITIES, LIENS, CAUSES OF ACTION, DEMANDS,
376
+ OBLIGATIONS, DISPUTES, DAMAGES, LOSSES, EXPENSES, ATTORNEYS’ FEES, COSTS,
377
+ LIABILITIES, AND ALL OTHER CLAIMS OF EVERY KIND AND NATURE WHATSOEVER, WHETHER
378
+ KNOWN OR UNKNOWN, ANTICIPATED OR UNANTICIPATED, FORESEEN OR UNFORESEEN, ACCRUED
379
+ OR UNACCRUED, DISCLOSED OR UNDISCLOSED, ARISING OUT OF OR RELATING TO LICENSEE’S
380
+ USE OF THE SOFTWARE. NOTHING IN THIS SECTION SHOULD BE INTERPRETED TO REQUIRE
381
+ LICENSEE TO INDEMNIFY LICENSOR, NOR REQUIRE LICENSOR TO INDEMNIFY LICENSEE.
382
+
383
+ 7. TERMINATION
384
+
385
+ 7.1. Violations of Ethical Standards or Breaching Duty of Care: If Licensee
386
+ violates the Ethical Standards section or Licensee, or any other person or
387
+ entity within the Supply Chain prior to a good or service reaching the Licensee,
388
+ breaches its Duty of Care to Supply Chain Impacted Parties, Licensee must remedy
389
+ the violation or harm caused by Licensee within 30 days of being notified of the
390
+ violation or harm. If Licensee fails to remedy the violation or harm within 30
391
+ days, all rights in the Software granted to Licensee by License will be null and
392
+ void as between Licensor and Licensee.
393
+
394
+ 7.2. Failure of Notice: If any person or entity notifies Licensee in writing
395
+ that Licensee has not complied with the Notice section of this License, Licensee
396
+ can keep this License by taking all practical steps to comply within 30 days
397
+ after the notice of noncompliance. If Licensee does not do so, Licensee’s
398
+ License (and all rights licensed hereunder) will end immediately.
399
+
400
+ 7.3. Judicial Findings: In the event Licensee is found by a civil, criminal,
401
+ administrative, or other court of competent jurisdiction, or some other
402
+ adjudicating body with legal authority, to have committed actions which are in
403
+ violation of the Ethical Standards or Supply Chain Impacted Party sections of
404
+ this License, all rights granted to Licensee by this License will terminate
405
+ immediately.
406
+
407
+ 7.4. Patent Litigation: If Licensee institutes patent litigation against any
408
+ entity (including a cross-claim or counterclaim in a suit) alleging that the
409
+ Software, all or part of the Software’s code, or a derivative work developed
410
+ using the Software, including a portion of its code, constitutes direct or
411
+ contributory patent infringement, then any patent license, along with all other
412
+ rights, granted to Licensee under this License will terminate as of the date
413
+ such litigation is filed.
414
+
415
+ 7.5. Additional Remedies: Termination of the License by failing to remedy harms
416
+ in no way prevents Licensor or Supply Chain Impacted Party from seeking
417
+ appropriate remedies at law or in equity.
418
+
419
+ 8. MISCELLANEOUS:
420
+
421
+ 8.1. Conditions: Sections 3, 4.1, 5.1, 5.2, 7.1, 7.2, 7.3, and 7.4 are
422
+ conditions of the rights granted to Licensee in the License.
423
+
424
+ 8.2. Equitable Relief: Licensor and any Supply Chain Impacted Party shall be
425
+ entitled to equitable relief, including injunctive relief or specific
426
+ performance of the terms hereof, in addition to any other remedy to which they
427
+ are entitled at law or in equity.
428
+
429
+ 8.3. Copyleft: Modified software, source code, or other derivative work must be
430
+ licensed, in its entirety, under the exact same conditions as this License.
431
+
432
+ 8.4. Severability: If any term or provision of this License is determined to be
433
+ invalid, illegal, or unenforceable by a court of competent jurisdiction, any
434
+ such determination of invalidity, illegality, or unenforceability shall not
435
+ affect any other term or provision of this License or invalidate or render
436
+ unenforceable such term or provision in any other jurisdiction. If the
437
+ determination of invalidity, illegality, or unenforceability by a court of
438
+ competent jurisdiction pertains to the terms or provisions contained in the
439
+ Ethical Standards section of this License, all rights in the Software granted to
440
+ Licensee shall be deemed null and void as between Licensor and Licensee.
441
+
442
+ 8.5. Section Titles: Section titles are solely written for organizational
443
+ purposes and should not be used to interpret the language within each section.
444
+
445
+ 8.6. Citations: Citations are solely written to provide context for the source
446
+ of the provisions in the Ethical Standards.
447
+
448
+ 8.7. Section Summaries: Some sections have a brief italicized description which
449
+ is provided for the sole purpose of briefly describing the section and should
450
+ not be used to interpret the terms of the License.
451
+
452
+ 8.8. Entire License: This is the entire License between the Licensor and
453
+ Licensee with respect to the claims released herein and that the consideration
454
+ stated herein is the only consideration or compensation to be paid or exchanged
455
+ between them for this License. This License cannot be modified or amended except
456
+ in a writing signed by Licensor and Licensee.
457
+
458
+ 8.9. Successors and Assigns: This License shall be binding upon and inure to the
459
+ benefit of the Licensor’s and Licensee’s respective heirs, successors, and
460
+ assigns.
461
+ ---------------------- END OFFICIAL HIPPOCRATIC LICENSE 3.0 TEXT ----------------------
462
+
463
+ ==============================================================================
464
+ Attribution, commercial use, and warranty
465
+ ==============================================================================
466
+
467
+ Attribution: cite this work as described in the model card (README.md), section
468
+ "How to cite". Attribution to Lowdown Labs is required under CC BY-NC 4.0.
469
+
470
+ Commercial use: CC BY-NC 4.0 does not grant commercial rights. Commercial licenses are
471
+ sold by Lowdown Labs; contact Lowdown Labs to purchase one.
472
+
473
+ No warranty: this work is provided as is, without warranty of any kind. See the model card
474
+ for the intended use, the evaluated conditions, and the known limitations.
README.md ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: other
3
+ license_name: lowdown-labs-lovely-license-1.0
4
+ license_link: LICENSE
5
+ tags:
6
+ - fela
7
+ - fourier-neural-operator
8
+ - fno
9
+ - cpu
10
+ - on-device
11
+ - pde-surrogate
12
+ - thermal-simulation
13
+ - battery
14
+ library_name: transformers
15
+ pipeline_tag: image-to-image
16
+ ---
17
+
18
+ # DISCLAIMER
19
+
20
+ This model is a research preview. Lowdown Labs has put together
21
+ this model in the interest of advancing public science.
22
+
23
+ # FELA PDE: on device 2D thermal field surrogate for battery packs
24
+
25
+ Give FELA PDE the layout of a battery pack, its heat load, and how it is being cooled, and it
26
+ tells you where the pack runs hot. It returns the full steady state temperature map in one fast
27
+ pass, standing in for a slower finite volume solve. It runs on a plain CPU with no GPU, so it can
28
+ sit inside a battery management tool, a design loop, or an on premises engineering app and flag
29
+ hot spots without a cloud round trip.
30
+
31
+ What ships in this repo is the small web lite version: 892,545 parameters, about 7.1 MB in fp32.
32
+ The larger validated teacher is a separate line and is not shipped here.
33
+
34
+ # What goes in, what comes out
35
+
36
+ - Input: an 8 channel physics field on a 96x96 grid, shape `(1, 8, 96, 96)`. The channels, in
37
+ order, are `mask` (pack solid region), `q_source` (volumetric heat source, the hot spot),
38
+ `k_field` (thermal conductivity), `h_conv` (convective heat transfer coefficient), `T_amb`
39
+ (ambient temperature), `x_coord`, `y_coord` (normalized 0..1 coordinates), and
40
+ `log_domain_L` (log of the physical domain size). Each channel is standardized with the
41
+ training statistics that ship in `config.json`; `modeling.preprocess` does this for you.
42
+ - Output: a 1 channel normalized temperature field, shape `(1, 1, 96, 96)`.
43
+ `modeling.denormalize` converts it to degrees Celsius using the training y statistics
44
+ (`Y_degC = Ynorm * y_std + y_mean`).
45
+ - In plain terms: give it the pack geometry, the heat load, and the cooling conditions, and it
46
+ returns the predicted temperature map so an engineer can see where the pack runs hot.
47
+
48
+ # Building an input (for battery and BMS engineers)
49
+
50
+ You do not hand build the 8 channel tensor. `input_builder.py` (an add on shipped in this repo)
51
+ builds it from ordinary pack parameters, matching the exact encoding the model was trained on
52
+ (the coordinate planes, `log_domain_L`, and the per channel standardization from `config.json`).
53
+ Two of the channels and `log_domain_L` are model conventions, not physics you supply. The
54
+ physical channels and their units are:
55
+
56
+ | Channel | Meaning | Units | Typical range |
57
+ |---|---|---|---|
58
+ | mask | 1 inside a cell, 0 in the coolant | none | 0 or 1 |
59
+ | q_source | heat source density in the cells | W/m3 | derived from current, SoC, R0 |
60
+ | k_field | thermal conductivity | W/(m K) | cell 1 to 30, coolant 0.1 to 1.5 |
61
+ | h_conv | convective heat transfer coefficient | W/(m2 K) | 5 to 200 |
62
+ | T_amb | ambient temperature | degC | 15 to 40 |
63
+ | x_coord | normalized column position (the builder sets this) | none | 0 to 1 |
64
+ | y_coord | normalized row position (the builder sets this) | none | 0 to 1 |
65
+ | log_domain_L | natural log of the physical pack size (the builder sets this) | ln(m) | pack 0.02 to 0.12 m |
66
+
67
+ The model was trained on this distribution; inputs well outside these ranges are not characterized.
68
+
69
+ ## The BMS path: from_pack
70
+
71
+ Give it a cell layout and pack parameters. It computes the heat source
72
+ (`P = current^2 * R0 * (1 + beta * (1 - SoC)^2)`, spread over the cell area), the conductivity map,
73
+ and the rest, then returns a ready to run `(1, 8, 96, 96)` tensor:
74
+
75
+ ```python
76
+ import torch
77
+ from input_builder import from_pack, cylinder_mask
78
+ from modeling import load_model, denormalize
79
+
80
+ model = load_model(".")
81
+ mask = cylinder_mask(rows=3, cols=4, radius_frac=0.4) # a 3 by 4 cylindrical cell pack
82
+ x = from_pack(
83
+ mask,
84
+ current_A=40.0, soc=0.3, R0_ohm=0.02,
85
+ k_cell_W_mK=20.0, k_coolant_W_mK=0.6,
86
+ h_conv_W_m2K=80.0, T_amb_degC=25.0, domain_L_m=0.08,
87
+ )
88
+ with torch.no_grad():
89
+ T = denormalize(model(x))[0, 0] # a 96 by 96 temperature map in degC
90
+ print("peak", float(T.max()), "degC")
91
+ ```
92
+
93
+ `cylinder_mask(rows, cols, radius_frac)` and `rect_mask(aspect, fill)` build the geometry mask.
94
+ `example.py` runs this end to end and prints the peak temperature and hottest cell.
95
+
96
+ ## The field path: from_fields
97
+
98
+ If you already have physical field maps (say from your own thermal model), pass them directly
99
+ instead of pack parameters:
100
+
101
+ ```python
102
+ from input_builder import from_fields
103
+ x = from_fields(mask, q_source_W_m3, k_field_W_mK, h_conv_W_m2K, T_amb_degC, domain_L_m)
104
+ ```
105
+
106
+ Each argument is a 96 by 96 array or a scalar (scalars are broadcast). The builder grids each to
107
+ 96 by 96, adds the coordinate and size channels, standardizes, and returns the model ready tensor.
108
+ It is verified to reproduce the training encoding exactly.
109
+
110
+ ## From a file: from_csv and from_json
111
+
112
+ If your pack parameters live in a file, point the builder at it. `pack.csv` (a header row plus one
113
+ values row) or `pack.json` (a flat object) use the same field names as `from_pack`, plus a geometry
114
+ (`rows`, `cols`, `radius_frac` for a cylindrical pack, or `aspect`, `fill` for a prismatic block):
115
+
116
+ ```python
117
+ import torch
118
+ from input_builder import from_csv
119
+ from modeling import load_model, denormalize
120
+
121
+ x = from_csv("pack.csv") # from_json("pack.json") works the same way
122
+ with torch.no_grad():
123
+ T = denormalize(load_model(".")(x))[0, 0]
124
+ ```
125
+
126
+ Example `pack.csv` and `pack.json` ship in this repo.
127
+
128
+ NB - real battery data comes in many shapes this repo does not read yet, such as
129
+ vendor spreadsheets with their own columns, CAD geometry like STEP or STL, and simulation exports
130
+ from tools like COMSOL or ANSYS. `from_csv` and `from_json` handle the flat parameter case, which
131
+ is the common one. If you already have a geometry or a field as numbers, load it into a 96 by 96
132
+ array yourself and pass it to `from_fields`.
133
+
134
+ # Why we built it this way
135
+
136
+ A temperature field is smooth and slowly varying, so we mix information in the frequency domain
137
+ rather than pixel by pixel. That is what a Fourier Neural Operator does, it learns filters that
138
+ act on the field's frequencies (an FFT, a learned filter, an inverse FFT), which suits a smooth
139
+ solution field well. The model is small and has no all pairs attention. One forward pass produces
140
+ the whole 96x96 map on a plain CPU, far faster than solving the same field directly with a
141
+ finite volume method.
142
+
143
+ # Architecture
144
+
145
+ - 2D FNO: a lifting `Conv2d(8 -> 32, 1x1)`, then 3 spectral plus pointwise residual blocks
146
+ (`SpectralConv2d` keeping 12x12 low and high Fourier modes plus a `Conv2d(32,32,1x1)` skip, GELU
147
+ residual), then a projection head `Conv2d(32 -> 128, 1x1) -> GELU -> Conv2d(128 -> 1, 1x1)`.
148
+ - 892,545 parameters. The full architecture is in `modeling.py`; the arch dims and
149
+ normalization statistics are in `config.json`.
150
+
151
+ # Training data
152
+
153
+ - Self generated. Every training, validation, and test sample is produced on CPU by a
154
+ deterministic steady state heat equation finite volume solver (pure NumPy and SciPy). No external
155
+ dataset is downloaded, scraped, or redistributed. The PDE, the geometry parameterization, and
156
+ the input and target encoding are reproduced in `train.py` (`--smoke` regenerates the split and
157
+ asserts the sizes). Full details, seeds, and the split are in `train.py`.
158
+ - License: none required. The generator is our own code; the finite volume method and the
159
+ Fourier Neural Operator (Li et al., ICLR 2021) are published methods, not licensed data.
160
+ Commercially clean.
161
+
162
+ The shipped `train.py` reproduces the primary battery 2D training recipe (a larger `FNO2dV32`
163
+ teacher with a length scale channel and an energy balance peak prior). The weights shipped here
164
+ are the distilled web lite student described above.
165
+
166
+ # How to run it
167
+
168
+ ```python
169
+ from huggingface_hub import hf_hub_download
170
+ import modeling
171
+
172
+ path = hf_hub_download("lowdown-labs/fela-pde", "model.safetensors")
173
+ model = modeling.load_model(path) # or load_model("/path/to/weights_dir")
174
+
175
+ # raw_field: an (8, 96, 96) physics field in physical units
176
+ x = modeling.preprocess(raw_field) # standardizes and validates shape
177
+ import torch
178
+ with torch.no_grad():
179
+ y_norm = model(x) # (1, 1, 96, 96) normalized temperature
180
+ y_degc = modeling.denormalize(y_norm) # degrees Celsius
181
+ ```
182
+
183
+ `verify.py` runs a fixed sample input and checks the output shape and a verification value.
184
+
185
+ # Serving artifacts
186
+
187
+ - `model.safetensors` plus `config.json` for the safetensors load path (fp32).
188
+ - `verify.py` runs a fixed sample input and checks the output shape and a verification value.
189
+
190
+ # Intended use, limitations, and safety
191
+
192
+ - This is a surrogate, not a certified thermal safety tool. Use its output as a fast screening
193
+ and design aid, not as the sole basis for a safety critical decision. Validate against a full
194
+ solver on your own configurations before relying on it.
195
+ - Trained and evaluated only on the self generated battery thermal distribution described above.
196
+ Geometries, materials, and boundary conditions outside that distribution are not characterized
197
+ here.
198
+ - This is the distilled web lite student. The larger validated checkpoints (battery and heatsink,
199
+ 2D and 3D) are a separate line.
200
+
201
+ # Acknowledgements and references
202
+
203
+ - Fourier Neural Operator: Li, Z., Kovachki, N., Azizzadenesheli, K., et al. (2021). Fourier
204
+ Neural Operator for Parametric Partial Differential Equations. ICLR.
205
+ https://arxiv.org/abs/2010.08895
206
+ - Finite volume heat transfer: Patankar, S. V. (1980). Numerical Heat Transfer and Fluid Flow.
207
+ - SciPy: Virtanen, P., et al. (2020). Nature Methods 17, 261-272.
208
+ - PyTorch: Paszke, A., et al. (2019). NeurIPS. https://arxiv.org/abs/1912.01703
209
+
210
+ # Model family
211
+
212
+ This is part of the FELA family from Lowdown Labs: one Fourier Neural Operator architecture
213
+ across many modalities, all CPU native and subquadratic. Sibling repos are independently
214
+ trained per modality and share no weights, so none carries a `base_model` link.
215
+
216
+ # License
217
+
218
+ Released under the Lowdown Labs Lovely License 1.0 (CC BY-NC 4.0 plus Hippocratic License 3.0). See LICENSE. For most LL models, a commercial license may be available; contact Lowdown Labs.
config.json ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "fela_pde_fno2d",
3
+ "description": "battery pack thermal surrogate; 8 channel physics field in, temperature field out",
4
+ "architecture": "fno2d",
5
+ "framework": "pytorch",
6
+ "arch": {
7
+ "in_ch": 8,
8
+ "out_ch": 1,
9
+ "width": 32,
10
+ "modes": 12,
11
+ "layers": 3,
12
+ "proj_hidden": 128,
13
+ "grid_h": 96,
14
+ "grid_w": 96
15
+ },
16
+ "params": 892545,
17
+ "input_shape": [
18
+ 1,
19
+ 8,
20
+ 96,
21
+ 96
22
+ ],
23
+ "output_shape": [
24
+ 1,
25
+ 1,
26
+ 96,
27
+ 96
28
+ ],
29
+ "weights_safetensors": "model.safetensors",
30
+ "input_channels": [
31
+ "mask",
32
+ "q_source",
33
+ "k_field",
34
+ "h_conv",
35
+ "T_amb",
36
+ "x_coord",
37
+ "y_coord",
38
+ "log_domain_L"
39
+ ],
40
+ "norm": {
41
+ "x_mean": [
42
+ 0.4555675685405731,
43
+ 33879.58984375,
44
+ 4.985158443450928,
45
+ 58.90073013305664,
46
+ 27.50713348388672,
47
+ 0.5,
48
+ 0.5,
49
+ -2.6601810455322266
50
+ ],
51
+ "x_std": [
52
+ 0.4980228543281555,
53
+ 89317.828125,
54
+ 6.717851638793945,
55
+ 51.0863151550293,
56
+ 7.186279773712158,
57
+ 0.29169902205467224,
58
+ 0.29169902205467224,
59
+ 0.3988337218761444
60
+ ],
61
+ "y_mean": 34.82331466674805,
62
+ "y_std": 18.65639305114746,
63
+ "amb_channel": 4
64
+ },
65
+ "interface": "Ynorm = model((X - x_mean)/x_std); Y_degC = Ynorm*y_std + y_mean",
66
+ "license": "lowdown-labs-lovely-license-1.0",
67
+ "architectures": [
68
+ "FelaPdeModel"
69
+ ],
70
+ "auto_map": {
71
+ "AutoConfig": "configuration_pde.FelaPdeConfig",
72
+ "AutoModel": "modeling_pde.FelaPdeModel"
73
+ },
74
+ "complex_keys": [
75
+ "sp.0.w1",
76
+ "sp.0.w2",
77
+ "sp.1.w1",
78
+ "sp.1.w2",
79
+ "sp.2.w1",
80
+ "sp.2.w2"
81
+ ]
82
+ }
configuration_pde.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import PretrainedConfig
2
+
3
+
4
+ class FelaPdeConfig(PretrainedConfig):
5
+ model_type = "fela_pde_fno2d"
6
+
7
+ def __init__(
8
+ self,
9
+ in_ch=8,
10
+ out_ch=1,
11
+ width=32,
12
+ modes=12,
13
+ layers=3,
14
+ proj_hidden=128,
15
+ grid_h=96,
16
+ grid_w=96,
17
+ arch=None,
18
+ **kwargs,
19
+ ):
20
+ if isinstance(arch, dict):
21
+ in_ch = arch.get("in_ch", in_ch)
22
+ out_ch = arch.get("out_ch", out_ch)
23
+ width = arch.get("width", width)
24
+ modes = arch.get("modes", modes)
25
+ layers = arch.get("layers", layers)
26
+ proj_hidden = arch.get("proj_hidden", proj_hidden)
27
+ grid_h = arch.get("grid_h", grid_h)
28
+ grid_w = arch.get("grid_w", grid_w)
29
+ self.in_ch = in_ch
30
+ self.out_ch = out_ch
31
+ self.width = width
32
+ self.modes = modes
33
+ self.layers = layers
34
+ self.proj_hidden = proj_hidden
35
+ self.grid_h = grid_h
36
+ self.grid_w = grid_w
37
+ self.arch = {
38
+ "in_ch": in_ch,
39
+ "out_ch": out_ch,
40
+ "width": width,
41
+ "modes": modes,
42
+ "layers": layers,
43
+ "proj_hidden": proj_hidden,
44
+ "grid_h": grid_h,
45
+ "grid_w": grid_w,
46
+ }
47
+ super().__init__(**kwargs)
example.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+ from input_builder import cylinder_mask, from_pack
4
+ from modeling import denormalize, load_model
5
+
6
+ model = load_model(".")
7
+
8
+ mask = cylinder_mask(rows=3, cols=4, radius_frac=0.4)
9
+ x = from_pack(
10
+ mask,
11
+ current_A=40.0,
12
+ soc=0.3,
13
+ R0_ohm=0.02,
14
+ k_cell_W_mK=20.0,
15
+ k_coolant_W_mK=0.6,
16
+ h_conv_W_m2K=80.0,
17
+ T_amb_degC=25.0,
18
+ domain_L_m=0.08,
19
+ )
20
+
21
+ with torch.no_grad():
22
+ T = denormalize(model(x))[0, 0]
23
+
24
+ hot = divmod(int(T.argmax()), T.shape[1])
25
+ print("temperature grid:", tuple(T.shape))
26
+ print("peak degC:", round(float(T.max()), 2))
27
+ print("mean degC:", round(float(T.mean()), 2))
28
+ print("hottest cell row,col:", [int(hot[0]), int(hot[1])])
input_builder.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import csv
2
+ import json
3
+
4
+ import numpy as np
5
+
6
+ from modeling import preprocess
7
+
8
+ N = 96
9
+
10
+
11
+ def _coords():
12
+ yy, xx = np.meshgrid(np.linspace(0, 1, N), np.linspace(0, 1, N), indexing="ij")
13
+ return xx, yy
14
+
15
+
16
+ def _to_field(v):
17
+ a = np.asarray(v, dtype=np.float64)
18
+ if a.ndim == 0:
19
+ return np.full((N, N), float(a))
20
+ if a.shape == (N, N):
21
+ return a
22
+ yi = np.linspace(0, a.shape[0] - 1, N).round().astype(int)
23
+ xi = np.linspace(0, a.shape[1] - 1, N).round().astype(int)
24
+ return a[yi][:, xi]
25
+
26
+
27
+ def cylinder_mask(rows, cols, radius_frac=0.4):
28
+ xx, yy = _coords()
29
+ m = np.zeros((N, N))
30
+ px, py = 1.0 / cols, 1.0 / rows
31
+ r = radius_frac * min(px, py)
32
+ for i in range(rows):
33
+ cy = (i + 0.5) * py
34
+ for j in range(cols):
35
+ cx = (j + 0.5) * px
36
+ m[(xx - cx) ** 2 + (yy - cy) ** 2 <= r * r] = 1.0
37
+ return m
38
+
39
+
40
+ def rect_mask(aspect=1.0, fill=0.7):
41
+ xx, yy = _coords()
42
+ m = np.zeros((N, N))
43
+ hh = min(0.98, (fill / aspect) ** 0.5)
44
+ ww = min(0.98, aspect * hh)
45
+ x0, x1 = 0.5 - ww / 2, 0.5 + ww / 2
46
+ y0, y1 = 0.5 - hh / 2, 0.5 + hh / 2
47
+ m[(xx >= x0) & (xx <= x1) & (yy >= y0) & (yy <= y1)] = 1.0
48
+ return m
49
+
50
+
51
+ def from_fields(
52
+ mask, q_source_W_m3, k_field_W_mK, h_conv_W_m2K, T_amb_degC, domain_L_m
53
+ ):
54
+ m = (_to_field(mask) > 0.5).astype(np.float64)
55
+ q = _to_field(q_source_W_m3) * m
56
+ k = _to_field(k_field_W_mK)
57
+ h = _to_field(h_conv_W_m2K)
58
+ ta = _to_field(T_amb_degC)
59
+ xx, yy = _coords()
60
+ logL = np.full((N, N), float(np.log(domain_L_m)))
61
+ field = np.stack([m, q, k, h, ta, xx, yy, logL], 0).astype(np.float32)
62
+ return preprocess(field)
63
+
64
+
65
+ def from_pack(
66
+ mask,
67
+ current_A,
68
+ soc,
69
+ R0_ohm,
70
+ k_cell_W_mK,
71
+ k_coolant_W_mK,
72
+ h_conv_W_m2K,
73
+ T_amb_degC,
74
+ domain_L_m,
75
+ beta=2.0,
76
+ ):
77
+ m = (_to_field(mask) > 0.5).astype(np.float64)
78
+ hg = domain_L_m / (N - 1)
79
+ R_int = R0_ohm * (1.0 + beta * (1.0 - soc) ** 2)
80
+ P_total = current_A**2 * R_int
81
+ area = max(m.sum() * hg * hg, hg * hg)
82
+ q = m * (P_total / area)
83
+ k = np.where(m > 0, k_cell_W_mK, k_coolant_W_mK)
84
+ return from_fields(m, q, k, h_conv_W_m2K, T_amb_degC, domain_L_m)
85
+
86
+
87
+ def from_params(d):
88
+ if d.get("rows") not in (None, "") and d.get("cols") not in (None, ""):
89
+ mask = cylinder_mask(
90
+ int(float(d["rows"])),
91
+ int(float(d["cols"])),
92
+ float(d.get("radius_frac") or 0.4),
93
+ )
94
+ else:
95
+ mask = rect_mask(float(d.get("aspect") or 1.0), float(d.get("fill") or 0.7))
96
+ return from_pack(
97
+ mask,
98
+ float(d["current_A"]),
99
+ float(d["soc"]),
100
+ float(d["R0_ohm"]),
101
+ float(d["k_cell_W_mK"]),
102
+ float(d["k_coolant_W_mK"]),
103
+ float(d["h_conv_W_m2K"]),
104
+ float(d["T_amb_degC"]),
105
+ float(d["domain_L_m"]),
106
+ beta=float(d.get("beta") or 2.0),
107
+ )
108
+
109
+
110
+ def from_json(path):
111
+ with open(path) as f:
112
+ return from_params(json.load(f))
113
+
114
+
115
+ def from_csv(path):
116
+ with open(path) as f:
117
+ return from_params(next(csv.DictReader(f)))
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d7d0c9f3b6282896c4e4e21a453253561f7288390cd9f8e09c64d99cc81d6b0a
3
+ size 7110540
modeling.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.nn.functional as F
6
+
7
+ CONFIG = None
8
+
9
+
10
+ def _config():
11
+ global CONFIG
12
+ if CONFIG is None:
13
+ here = os.path.dirname(os.path.abspath(__file__))
14
+ with open(os.path.join(here, "config.json")) as f:
15
+ CONFIG = json.load(f)
16
+ return CONFIG
17
+
18
+
19
+ class SpectralConv2d(nn.Module):
20
+ def __init__(self, ci, co, m1, m2):
21
+ super().__init__()
22
+ self.m1, self.m2 = (m1, m2)
23
+ s = 1 / (ci * co)
24
+ self.w1 = nn.Parameter(s * torch.rand(ci, co, m1, m2, dtype=torch.cfloat))
25
+ self.w2 = nn.Parameter(s * torch.rand(ci, co, m1, m2, dtype=torch.cfloat))
26
+
27
+ def forward(self, x):
28
+ B, C, Hh, Ww = x.shape
29
+ xf = torch.fft.rfft2(x)
30
+ o = torch.zeros(
31
+ B, self.w1.shape[1], Hh, Ww // 2 + 1, dtype=torch.cfloat, device=x.device
32
+ )
33
+ o[:, :, : self.m1, : self.m2] = torch.einsum(
34
+ "bixy,ioxy->boxy", xf[:, :, : self.m1, : self.m2], self.w1
35
+ )
36
+ o[:, :, -self.m1 :, : self.m2] = torch.einsum(
37
+ "bixy,ioxy->boxy", xf[:, :, -self.m1 :, : self.m2], self.w2
38
+ )
39
+ return torch.fft.irfft2(o, s=(Hh, Ww))
40
+
41
+
42
+ class FNO2d(nn.Module):
43
+ def __init__(self, in_ch=8, modes=12, width=32, L=3, proj_hidden=128):
44
+ super().__init__()
45
+ self.lift = nn.Conv2d(in_ch, width, 1)
46
+ self.sp = nn.ModuleList(
47
+ [SpectralConv2d(width, width, modes, modes) for _ in range(L)]
48
+ )
49
+ self.w = nn.ModuleList([nn.Conv2d(width, width, 1) for _ in range(L)])
50
+ self.proj = nn.Sequential(
51
+ nn.Conv2d(width, proj_hidden, 1), nn.GELU(), nn.Conv2d(proj_hidden, 1, 1)
52
+ )
53
+
54
+ def forward(self, x):
55
+ h = self.lift(x)
56
+ for sp, w in zip(self.sp, self.w):
57
+ h = h + F.gelu(sp(h) + w(h))
58
+ return self.proj(h)
59
+
60
+
61
+ def validate_input(x):
62
+ a = _config()["arch"]
63
+ if not isinstance(x, torch.Tensor):
64
+ raise TypeError(f"Expected a torch.Tensor, got {type(x)}")
65
+ if (
66
+ x.dim() != 4
67
+ or x.shape[1] != a["in_ch"]
68
+ or x.shape[2] != a["grid_h"]
69
+ or (x.shape[3] != a["grid_w"])
70
+ ):
71
+ raise ValueError(
72
+ f"Expected an input of shape (batch, {a['in_ch']}, {a['grid_h']}, {a['grid_w']}), got {tuple(x.shape)}."
73
+ )
74
+ return x
75
+
76
+
77
+ def preprocess(raw_field, mean=None, std=None):
78
+ x = torch.as_tensor(raw_field, dtype=torch.float32)
79
+ if x.dim() == 3:
80
+ x = x.unsqueeze(0)
81
+ norm = _config()["norm"]
82
+ m = torch.as_tensor(
83
+ mean if mean is not None else norm["x_mean"], dtype=torch.float32
84
+ ).reshape(1, -1, 1, 1)
85
+ s = torch.as_tensor(
86
+ std if std is not None else norm["x_std"], dtype=torch.float32
87
+ ).reshape(1, -1, 1, 1)
88
+ x = (x - m) / torch.clamp(s, min=1e-06)
89
+ return validate_input(x)
90
+
91
+
92
+ def denormalize(y_norm):
93
+ norm = _config()["norm"]
94
+ return torch.as_tensor(y_norm, dtype=torch.float32) * norm["y_std"] + norm["y_mean"]
95
+
96
+
97
+ def _build():
98
+ a = _config()["arch"]
99
+ return FNO2d(
100
+ in_ch=a["in_ch"],
101
+ modes=a["modes"],
102
+ width=a["width"],
103
+ L=a["layers"],
104
+ proj_hidden=a["proj_hidden"],
105
+ )
106
+
107
+
108
+ def load_model(path_or_repo, filename=None):
109
+ path = path_or_repo
110
+ fname = filename or _config()["weights_safetensors"]
111
+ if os.path.isdir(path):
112
+ path = os.path.join(path, fname)
113
+ elif not os.path.exists(path):
114
+ from huggingface_hub import hf_hub_download
115
+
116
+ path = hf_hub_download(path_or_repo, fname)
117
+ from safetensors.torch import load_file
118
+
119
+ state = load_file(path)
120
+ cplx = set(_config().get("complex_keys", []))
121
+ state = {
122
+ k: (torch.view_as_complex(v.contiguous()) if k in cplx else v)
123
+ for k, v in state.items()
124
+ }
125
+ model = _build()
126
+ model.load_state_dict(state, strict=True)
127
+ model.eval()
128
+ return model
129
+
130
+
131
+ def from_pretrained(repo_id):
132
+ return load_model(repo_id)
modeling_pde.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import types
4
+
5
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
6
+ import torch
7
+ import torch.nn as nn
8
+ from transformers import PreTrainedModel
9
+ from transformers.modeling_outputs import CausalLMOutput
10
+
11
+ from .configuration_pde import FelaPdeConfig
12
+ from .modeling import FNO2d, SpectralConv2d
13
+
14
+
15
+ def _spectral_forward(self, x):
16
+ w1 = torch.view_as_complex(self.w1)
17
+ w2 = torch.view_as_complex(self.w2)
18
+ B, C, Hh, Ww = x.shape
19
+ xf = torch.fft.rfft2(x)
20
+ o = torch.zeros(
21
+ B, w1.shape[1], Hh, Ww // 2 + 1, dtype=torch.cfloat, device=x.device
22
+ )
23
+ o[:, :, : self.m1, : self.m2] = torch.einsum(
24
+ "bixy,ioxy->boxy", xf[:, :, : self.m1, : self.m2], w1
25
+ )
26
+ o[:, :, -self.m1 :, : self.m2] = torch.einsum(
27
+ "bixy,ioxy->boxy", xf[:, :, -self.m1 :, : self.m2], w2
28
+ )
29
+ return torch.fft.irfft2(o, s=(Hh, Ww))
30
+
31
+
32
+ def _realify(model):
33
+ for m in model.modules():
34
+ if isinstance(m, SpectralConv2d):
35
+ m.w1 = nn.Parameter(torch.view_as_real(m.w1.detach()).contiguous())
36
+ m.w2 = nn.Parameter(torch.view_as_real(m.w2.detach()).contiguous())
37
+ m.forward = types.MethodType(_spectral_forward, m)
38
+
39
+
40
+ class FelaPdeModel(PreTrainedModel):
41
+ config_class = FelaPdeConfig
42
+ base_model_prefix = "model"
43
+ main_input_name = "x"
44
+
45
+ def __init__(self, config):
46
+ super().__init__(config)
47
+ self.model = FNO2d(
48
+ in_ch=config.in_ch,
49
+ modes=config.modes,
50
+ width=config.width,
51
+ L=config.layers,
52
+ proj_hidden=config.proj_hidden,
53
+ )
54
+ _realify(self.model)
55
+ self.post_init()
56
+
57
+ def forward(self, x=None, input_values=None, **kwargs):
58
+ if x is None:
59
+ x = input_values
60
+ out = self.model(x)
61
+ return CausalLMOutput(logits=out)
pack.csv ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ current_A,soc,R0_ohm,k_cell_W_mK,k_coolant_W_mK,h_conv_W_m2K,T_amb_degC,domain_L_m,rows,cols,radius_frac
2
+ 40,0.3,0.02,20,0.6,80,25,0.08,3,4,0.4
pack.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "current_A": 40,
3
+ "soc": 0.3,
4
+ "R0_ohm": 0.02,
5
+ "k_cell_W_mK": 20,
6
+ "k_coolant_W_mK": 0.6,
7
+ "h_conv_W_m2K": 80,
8
+ "T_amb_degC": 25,
9
+ "domain_L_m": 0.08,
10
+ "rows": 3,
11
+ "cols": 4,
12
+ "radius_frac": 0.4
13
+ }
streaming/manifest.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model": "pde",
3
+ "format": "fp16-streaming",
4
+ "note": "load order is smallest-first for progressive/streaming load",
5
+ "files": [
6
+ {
7
+ "file": "model_fp16.safetensors",
8
+ "source": "model.safetensors",
9
+ "dtype": "fp16",
10
+ "bytes": 3555962,
11
+ "approx_mb": 3.391
12
+ }
13
+ ]
14
+ }
streaming/model_fp16.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:66cc543317c054e7c2c7ed7541d12248c9732e5a26d4b0e4814e4357164dc916
3
+ size 3555962
train.py ADDED
@@ -0,0 +1,417 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ for _v in (
4
+ "OMP_NUM_THREADS",
5
+ "OPENBLAS_NUM_THREADS",
6
+ "MKL_NUM_THREADS",
7
+ "NUMEXPR_NUM_THREADS",
8
+ ):
9
+ os.environ.setdefault(_v, "1")
10
+ import sys, time, json, argparse
11
+ import numpy as np
12
+ import scipy.sparse as sp
13
+ from scipy.sparse.linalg import spsolve
14
+
15
+ N = 96
16
+ CH8 = [
17
+ "mask",
18
+ "q_source",
19
+ "k_field",
20
+ "h_conv",
21
+ "T_amb",
22
+ "x_coord",
23
+ "y_coord",
24
+ "log_domain_L",
25
+ ]
26
+ SEED_TRAIN = 12340000
27
+ SEED_HOT = 55000000
28
+ SEED_HOLDOUT = 900000000
29
+ SEED_EDGE = 900000000 + 500000000
30
+ RANGES = dict(
31
+ domain_L=(0.02, 0.12),
32
+ cyl_rows=(2, 5),
33
+ cyl_cols=(2, 5),
34
+ cell_radius_frac=(0.3, 0.48),
35
+ aspect=(0.4, 2.5),
36
+ cell_fill=(0.55, 0.9),
37
+ current=(1.0, 60.0),
38
+ soc=(0.05, 1.0),
39
+ R0=(0.005, 0.05),
40
+ beta=(0.5, 4.0),
41
+ k_cell=(1.0, 30.0),
42
+ k_coolant=(0.1, 1.5),
43
+ h_conv=(5.0, 200.0),
44
+ T_amb=(15.0, 40.0),
45
+ )
46
+
47
+
48
+ def build_system(N, hg, k_field, q_field, h_conv, T_amb):
49
+ n = N * N
50
+ dx2 = hg * hg
51
+ k = k_field.astype(np.float64)
52
+ q = q_field.astype(np.float64)
53
+
54
+ def hmean(a, b):
55
+ return 2.0 * a * b / (a + b + 1e-30)
56
+
57
+ ids = np.arange(n).reshape(N, N)
58
+ rows = []
59
+ cols = []
60
+ vals = []
61
+ diag = np.zeros((N, N))
62
+ b = q * dx2
63
+ kf = hmean(k[1:, :], k[:-1, :])
64
+ p = ids[1:, :].ravel()
65
+ nb = ids[:-1, :].ravel()
66
+ kk = kf.ravel()
67
+ rows.append(p)
68
+ cols.append(nb)
69
+ vals.append(-kk)
70
+ diag[1:, :] += kf
71
+ p = ids[:-1, :].ravel()
72
+ nb = ids[1:, :].ravel()
73
+ rows.append(p)
74
+ cols.append(nb)
75
+ vals.append(-kk)
76
+ diag[:-1, :] += kf
77
+ kf = hmean(k[:, 1:], k[:, :-1])
78
+ p = ids[:, 1:].ravel()
79
+ nb = ids[:, :-1].ravel()
80
+ kk = kf.ravel()
81
+ rows.append(p)
82
+ cols.append(nb)
83
+ vals.append(-kk)
84
+ diag[:, 1:] += kf
85
+ p = ids[:, :-1].ravel()
86
+ nb = ids[:, 1:].ravel()
87
+ rows.append(p)
88
+ cols.append(nb)
89
+ vals.append(-kk)
90
+ diag[:, :-1] += kf
91
+ hdx = h_conv * hg
92
+ bmask = np.zeros((N, N))
93
+ bmask[0, :] += 1
94
+ bmask[-1, :] += 1
95
+ bmask[:, 0] += 1
96
+ bmask[:, -1] += 1
97
+ diag += hdx * bmask
98
+ b += hdx * T_amb * bmask
99
+ rows.append(ids.ravel())
100
+ cols.append(ids.ravel())
101
+ vals.append(diag.ravel())
102
+ rows = np.concatenate(rows)
103
+ cols = np.concatenate(cols)
104
+ vals = np.concatenate(vals)
105
+ A = sp.csr_matrix((vals, (rows, cols)), shape=(n, n))
106
+ return (A, b.ravel())
107
+
108
+
109
+ def solve_steady(N, hg, k_field, q_field, h_conv, T_amb):
110
+ A, b = build_system(N, hg, k_field, q_field, h_conv, T_amb)
111
+ return spsolve(A.tocsc(), b).reshape(N, N)
112
+
113
+
114
+ def sample_params(rng):
115
+ p = {}
116
+ p["domain_L"] = rng.uniform(*RANGES["domain_L"])
117
+ p["geom"] = "cyl" if rng.random() < 0.6 else "pris"
118
+ p["current"] = rng.uniform(*RANGES["current"])
119
+ p["soc"] = rng.uniform(*RANGES["soc"])
120
+ p["R0"] = rng.uniform(*RANGES["R0"])
121
+ p["beta"] = rng.uniform(*RANGES["beta"])
122
+ p["k_cell"] = rng.uniform(*RANGES["k_cell"])
123
+ p["k_coolant"] = rng.uniform(*RANGES["k_coolant"])
124
+ p["h_conv"] = rng.uniform(*RANGES["h_conv"])
125
+ p["T_amb"] = rng.uniform(*RANGES["T_amb"])
126
+ if p["geom"] == "cyl":
127
+ p["rows"] = int(rng.integers(RANGES["cyl_rows"][0], RANGES["cyl_rows"][1] + 1))
128
+ p["cols"] = int(rng.integers(RANGES["cyl_cols"][0], RANGES["cyl_cols"][1] + 1))
129
+ p["rfrac"] = rng.uniform(*RANGES["cell_radius_frac"])
130
+ else:
131
+ p["aspect"] = rng.uniform(*RANGES["aspect"])
132
+ p["fill"] = rng.uniform(*RANGES["cell_fill"])
133
+ return p
134
+
135
+
136
+ def build_fields(N, p):
137
+ L = p["domain_L"]
138
+ hg = L / (N - 1)
139
+ yy, xx = np.meshgrid(np.linspace(0, 1, N), np.linspace(0, 1, N), indexing="ij")
140
+ mask = np.zeros((N, N), dtype=np.float64)
141
+ R_int = p["R0"] * (1.0 + p["beta"] * (1.0 - p["soc"]) ** 2)
142
+ P_total = p["current"] ** 2 * R_int
143
+ if p["geom"] == "cyl":
144
+ rows, cols = (p["rows"], p["cols"])
145
+ pitch_x = 1.0 / cols
146
+ pitch_y = 1.0 / rows
147
+ r = p["rfrac"] * min(pitch_x, pitch_y)
148
+ for i in range(rows):
149
+ cy = (i + 0.5) * pitch_y
150
+ for j in range(cols):
151
+ cx = (j + 0.5) * pitch_x
152
+ d = (xx - cx) ** 2 + (yy - cy) ** 2
153
+ mask[d <= r * r] = 1.0
154
+ else:
155
+ a = p["aspect"]
156
+ f = p["fill"]
157
+ hh = min(0.98, np.sqrt(f / a))
158
+ ww = min(0.98, a * hh)
159
+ x0, x1 = (0.5 - ww / 2, 0.5 + ww / 2)
160
+ y0, y1 = (0.5 - hh / 2, 0.5 + hh / 2)
161
+ mask[(xx >= x0) & (xx <= x1) & (yy >= y0) & (yy <= y1)] = 1.0
162
+ cell_area_phys = mask.sum() * hg * hg
163
+ if cell_area_phys <= 0:
164
+ cell_area_phys = hg * hg
165
+ q_field = mask * (P_total / cell_area_phys)
166
+ k_field = np.where(mask > 0, p["k_cell"], p["k_coolant"])
167
+ return (mask, k_field, q_field, hg)
168
+
169
+
170
+ def build_X(p, mask, k_field, q_field):
171
+ yy, xx = np.meshgrid(np.linspace(0, 1, N), np.linspace(0, 1, N), indexing="ij")
172
+ logL = np.full((N, N), np.log(p["domain_L"]), dtype=np.float64)
173
+ return np.stack(
174
+ [
175
+ mask,
176
+ q_field,
177
+ k_field,
178
+ np.full((N, N), p["h_conv"]),
179
+ np.full((N, N), p["T_amb"]),
180
+ xx,
181
+ yy,
182
+ logL,
183
+ ],
184
+ 0,
185
+ ).astype(np.float32)
186
+
187
+
188
+ def gen_uniform(seed):
189
+ rng = np.random.default_rng(seed)
190
+ p = sample_params(rng)
191
+ mask, k, q, hg = build_fields(N, p)
192
+ T = solve_steady(N, hg, k, q, p["h_conv"], p["T_amb"])
193
+ return (build_X(p, mask, k, q), T.astype(np.float32))
194
+
195
+
196
+ def run(fn, jobs, workers):
197
+ if workers <= 1:
198
+ out = [fn(j) for j in jobs]
199
+ else:
200
+ from multiprocessing import Pool
201
+
202
+ with Pool(workers) as pool:
203
+ out = list(pool.imap(fn, jobs, chunksize=8))
204
+ Xs, Ys = zip(*out)
205
+ return (np.stack(Xs), np.stack(Ys))
206
+
207
+
208
+ def make_split(n_train, workers):
209
+ Xtr, Ytr = run(gen_uniform, [SEED_TRAIN + i for i in range(n_train)], workers)
210
+ n = Xtr.shape[0]
211
+ perm = np.random.default_rng(0).permutation(n)
212
+ ntr = int(0.8 * n)
213
+ nva = int(0.1 * n)
214
+ sp_idx = {
215
+ "train": perm[:ntr],
216
+ "val": perm[ntr : ntr + nva],
217
+ "test": perm[ntr + nva :],
218
+ }
219
+ return (Xtr, Ytr, sp_idx)
220
+
221
+
222
+ def main():
223
+ ap = argparse.ArgumentParser()
224
+ ap.add_argument(
225
+ "--data", default="/workspace/pde_surrogate/battery/battery_thermal_v3.h5"
226
+ )
227
+ ap.add_argument(
228
+ "--hot", default="/workspace/pde_surrogate/battery/hot_enrich_v3.h5"
229
+ )
230
+ ap.add_argument(
231
+ "--out", default="/workspace/pde_surrogate/battery/fno_ckpt_v3_final.pt"
232
+ )
233
+ ap.add_argument("--n_train", type=int, default=4000)
234
+ ap.add_argument("--n_hot", type=int, default=5000)
235
+ ap.add_argument("--workers", type=int, default=32)
236
+ ap.add_argument("--epochs", type=int, default=300)
237
+ ap.add_argument("--bs", type=int, default=128)
238
+ ap.add_argument("--lr", type=float, default=0.002)
239
+ ap.add_argument("--modes", type=int, default=32)
240
+ ap.add_argument("--width", type=int, default=80)
241
+ ap.add_argument("--layers", type=int, default=4)
242
+ ap.add_argument("--scale_floor", type=float, default=2.0)
243
+ ap.add_argument("--grad_w", type=float, default=3.0)
244
+ ap.add_argument("--rise_w", type=float, default=1.5)
245
+ ap.add_argument("--scale_w", type=float, default=3.0)
246
+ ap.add_argument("--smoke", action="store_true")
247
+ args = ap.parse_args()
248
+ if args.smoke:
249
+ Xtr, Ytr, sp_idx = make_split(args.n_train, args.workers)
250
+ n = Xtr.shape[0]
251
+ assert Xtr.shape[1] == 8 and Xtr.shape[2] == N and (Xtr.shape[3] == N)
252
+ assert (
253
+ len(sp_idx["train"]) == 3200
254
+ and len(sp_idx["val"]) == 400
255
+ and (len(sp_idx["test"]) == 400)
256
+ )
257
+ xu, yu = gen_uniform(SEED_HOLDOUT)
258
+ assert xu.shape == (8, N, N) and np.isfinite(yu).all() and (yu.max() > yu.min())
259
+ print(
260
+ f"[Smoke] n={n} train={len(sp_idx['train'])} val={len(sp_idx['val'])} test={len(sp_idx['test'])} ch={len(CH8)} holdoutT[{yu.min():.2f},{yu.max():.2f}]",
261
+ flush=True,
262
+ )
263
+ return
264
+ import h5py, torch, torch.nn as nn, torch.nn.functional as F
265
+
266
+ sys.path.insert(0, "/workspace/pde_surrogate/scripts")
267
+ from train_fno_v32 import FNO2dV32, phys_prior, spatial_grad_mag
268
+
269
+ AMBCH = 4
270
+
271
+ def load_h5(path, split):
272
+ with h5py.File(path, "r") as f:
273
+ return (
274
+ torch.from_numpy(f[split]["X"][:].astype(np.float32)),
275
+ torch.from_numpy(f[split]["Y"][:].astype(np.float32)),
276
+ )
277
+
278
+ dev = "cuda"
279
+ with h5py.File(args.data, "r") as f:
280
+ x_mean = np.array(f.attrs["x_mean"], np.float32)
281
+ x_std = np.array(f.attrs["x_std"], np.float32)
282
+ y_mean = float(f.attrs["y_mean"])
283
+ y_std = float(f.attrs["y_std"])
284
+ channels = json.loads(f.attrs["channels"])
285
+ base_in = f["train"]["X"].shape[1]
286
+ Xtr, Ytr = load_h5(args.data, "train")
287
+ Xva, Yva = load_h5(args.data, "val")
288
+ with h5py.File(args.hot, "r") as f:
289
+ Xh = torch.from_numpy(f["hot"]["X"][:].astype(np.float32))
290
+ Yh = torch.from_numpy(f["hot"]["Y"][:].astype(np.float32))
291
+ Xtr = torch.cat([Xtr, Xh], 0)
292
+ Ytr = torch.cat([Ytr, Yh], 0)
293
+ xm_ = Xtr.reshape(Xtr.shape[0], base_in, -1).mean((0, 2))
294
+ xs_ = Xtr.reshape(Xtr.shape[0], base_in, -1).std((0, 2)) + 1e-06
295
+ x_mean = xm_.numpy()
296
+ x_std = xs_.numpy()
297
+ Xtr = Xtr.to(dev)
298
+ Ytr = Ytr.to(dev)
299
+ Xva = Xva.to(dev)
300
+ Yva = Yva.to(dev)
301
+ ntr = Xtr.shape[0]
302
+ xm = torch.tensor(x_mean, device=dev)[None, :, None, None]
303
+ xs = torch.tensor(x_std, device=dev)[None, :, None, None]
304
+ model = FNO2dV32(
305
+ in_ch=base_in, modes=args.modes, width=args.width, L=args.layers
306
+ ).to(dev)
307
+ opt = torch.optim.AdamW(model.parameters(), lr=args.lr, weight_decay=1e-05)
308
+ sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, args.epochs)
309
+ print(
310
+ f"[V32] params={sum((p.numel() for p in model.parameters())) / 1000000.0:.2f}M ntr={ntr}",
311
+ flush=True,
312
+ )
313
+
314
+ def prep(Xb):
315
+ dTc, dTd = phys_prior(Xb)
316
+ return ((Xb - xm) / xs, dTc, dTd)
317
+
318
+ def val_binned():
319
+ model.eval()
320
+ rls = []
321
+ rises = []
322
+ mae = 0.0
323
+ vb = 0
324
+ with torch.no_grad():
325
+ for s in range(0, Xva.shape[0], args.bs):
326
+ Xb = Xva[s : s + args.bs]
327
+ Yb = Yva[s : s + args.bs][:, None]
328
+ Xin, dTc, dTd = prep(Xb)
329
+ field_ps, log_scale, _ = model(Xin, dTc, dTd)
330
+ amb = Xb[:, AMBCH].flatten(1).mean(1)
331
+ log_scale = log_scale.clamp(
332
+ float(np.log(args.scale_floor)), float(np.log(2000.0))
333
+ )
334
+ pp = (
335
+ amb[:, None, None, None]
336
+ + torch.exp(log_scale)[:, None, None, None] * field_ps
337
+ )
338
+ num = torch.linalg.norm((pp - Yb).flatten(1), dim=1)
339
+ den = torch.linalg.norm(Yb.flatten(1), dim=1).clamp_min(1e-08)
340
+ rls.append((num / den).cpu())
341
+ rises.append((Yb.flatten(1).amax(1) - amb).cpu())
342
+ mae += torch.abs(pp - Yb).mean().item()
343
+ vb += 1
344
+ rls = torch.cat(rls)
345
+ rises = torch.cat(rises)
346
+ mae /= vb
347
+ hot = rls[rises > 10]
348
+ return (
349
+ float(rls.median()),
350
+ float(hot.median()) if hot.numel() else float("nan"),
351
+ mae,
352
+ int((rises > 10).sum()),
353
+ )
354
+
355
+ best = 1000000000.0
356
+ for ep in range(args.epochs):
357
+ model.train()
358
+ perm = torch.randperm(ntr, device=dev)
359
+ tl = 0.0
360
+ nb = 0
361
+ for s in range(0, ntr, args.bs):
362
+ idx = perm[s : s + args.bs]
363
+ Xb = Xtr[idx]
364
+ Yb = Ytr[idx][:, None]
365
+ amb = Xb[:, AMBCH].flatten(1).mean(1)
366
+ flat = Yb.flatten(1)
367
+ scale = torch.quantile(flat - amb[:, None], 0.95, dim=1).clamp_min(
368
+ args.scale_floor
369
+ )
370
+ Yps = (Yb - amb.view(-1, 1, 1, 1)) / scale.view(-1, 1, 1, 1)
371
+ Xin, dTc, dTd = prep(Xb)
372
+ opt.zero_grad()
373
+ field_ps, log_scale, prior = model(Xin, dTc, dTd)
374
+ gm = spatial_grad_mag(Yps)
375
+ gw = 1.0 + args.grad_w * gm / (
376
+ gm.flatten(1).mean(1)[:, None, None, None] + 1e-06
377
+ )
378
+ rise = (flat.amax(1) - amb).clamp_min(0.0)
379
+ sw = ((rise + 1.0) ** args.rise_w)[:, None, None, None]
380
+ sw = sw / sw.mean()
381
+ field_loss = (gw * sw * (field_ps - Yps) ** 2).mean()
382
+ scale_loss = F.smooth_l1_loss(log_scale, torch.log(scale), beta=0.1)
383
+ loss = field_loss + args.scale_w * scale_loss
384
+ loss.backward()
385
+ opt.step()
386
+ tl += loss.item()
387
+ nb += 1
388
+ sched.step()
389
+ overall, hotm, mae, nhot = val_binned()
390
+ score = hotm if hotm == hotm else overall
391
+ if score < best:
392
+ best = score
393
+ torch.save(
394
+ {
395
+ "model": model.state_dict(),
396
+ "args": vars(args),
397
+ "x_mean": x_mean,
398
+ "x_std": x_std,
399
+ "y_mean": y_mean,
400
+ "y_std": y_std,
401
+ "channels": channels,
402
+ "scale_floor": args.scale_floor,
403
+ "amb_channel": AMBCH,
404
+ "in_ch": base_in,
405
+ },
406
+ args.out,
407
+ )
408
+ if ep % 10 == 0 or ep == args.epochs - 1:
409
+ print(
410
+ f"Ep{ep:3d} loss={tl / nb:.4e} overall={overall:.4f} HOTSPOT(>10C,n={nhot})={hotm:.4f} MAE={mae:.3f}C (best {best:.4f})",
411
+ flush=True,
412
+ )
413
+ print(f"[V32] DONE best HOTSPOT {best:.4f} -> {args.out}", flush=True)
414
+
415
+
416
+ if __name__ == "__main__":
417
+ main()
verify.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import os
3
+ import sys
4
+ import torch
5
+
6
+ sys.path.insert(0, os.path.dirname(__file__))
7
+ from modeling import load_model
8
+
9
+ SHAPE = (1, 8, 96, 96)
10
+ VERIFICATION = 0.566771
11
+ TOL = 0.001
12
+
13
+
14
+ def fixed_input():
15
+ torch.manual_seed(0)
16
+ return torch.randn(*SHAPE)
17
+
18
+
19
+ def main():
20
+ ap = argparse.ArgumentParser()
21
+ ap.add_argument("--weights", default=".")
22
+ args = ap.parse_args()
23
+ model = load_model(args.weights)
24
+ x = fixed_input()
25
+ with torch.no_grad():
26
+ out = model(x)
27
+ if tuple(out.shape) != (1, 1, 96, 96):
28
+ print(
29
+ f"Fail: unexpected output shape {tuple(out.shape)}, expected (1, 1, 96, 96)"
30
+ )
31
+ sys.exit(1)
32
+ center = out[0, 0, 48, 48].item()
33
+ print(f"Captured center value: {center:.6f}")
34
+ if abs(center - VERIFICATION) > TOL:
35
+ print(
36
+ f"Fail: center {center:.6f} differs from verification {VERIFICATION:.6f} by more than {TOL}"
37
+ )
38
+ sys.exit(1)
39
+ print(f"Verification check OK (center within {TOL} of {VERIFICATION:.6f})")
40
+ sys.exit(0)
41
+
42
+
43
+ if __name__ == "__main__":
44
+ main()