itstheraj commited on
Commit
031d0d7
·
0 Parent(s):

initial commit

Browse files
.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,378 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ - predictive-maintenance
12
+ - time-series
13
+ - anomaly-detection
14
+ library_name: transformers
15
+ pipeline_tag: time-series-forecasting
16
+ ---
17
+
18
+ # DISCLAIMER
19
+
20
+ This model is a research preview. The CWRU bearing dataset publishes no explicit license and
21
+ grants no commercial use rights, so respect that before any commercial use. Lowdown Labs has
22
+ put together this model in the interest of advancing public science.
23
+
24
+ # FELA-PdM: on device predictive maintenance for rotating machines
25
+
26
+ FELA-PdM watches the raw signal from a vibration or sensor stream on a machine (a bearing,
27
+ motor, pump, gearbox, or engine) and tells a maintenance team two things: what is wrong, and
28
+ how much longer the machine is likely to keep running.
29
+
30
+ It is small enough to run on a
31
+ $3 to $10 microcontroller sitting next to the sensor, so a plant does not have to stream raw
32
+ data to the cloud.
33
+
34
+ # What goes in, what comes out
35
+
36
+ There are two trained tasks. Pick by the question you are asking. The bearing fault task ships as
37
+ one head (CWRU); the remaining useful life task ships as four heads, one per C-MAPSS subset
38
+ (FD001 to FD004).
39
+
40
+ - Bearing fault classification (trained on CWRU): in is a window of vibration samples from an
41
+ accelerometer, shape `(1, 2048, 1)` (2048 raw samples, one channel, sampled at 12 kHz).
42
+ Out is a fault class, one of healthy, inner race defect, rolling element (ball) defect, or
43
+ outer race defect, at one of three defect sizes (0.007, 0.014, 0.021 inch); 10 classes
44
+ total. In plain terms: "this bearing has an inner race defect" or "this bearing is healthy."
45
+ - Remaining useful life (trained on NASA C-MAPSS turbofan): in is a short history of per cycle
46
+ sensor readings, shape `(1, 30, 14)` (30 cycles, 14 sensors). Out is an estimate of how many
47
+ operating cycles remain before failure. A reliability engineer reads it as "this unit has
48
+ roughly N cycles left, plan the swap."
49
+
50
+ # Why we built it this way
51
+
52
+ The sequence mixer is a Fourier Neural Operator, a filter the model learns and applies in the
53
+ frequency domain. A failing bearing or gear shows up as periodic, high frequency vibration, and
54
+ reading frequencies is exactly what this kind of operator does well, so it fits the problem. There
55
+ is no all pairs attention, so the working memory stays small and fixed however long the machine
56
+ runs. That is what lets it sit on a cheap microcontroller next to the sensor, on a battery or
57
+ panel powered node, with no cloud connection.
58
+
59
+ # Performance
60
+
61
+ Speed and footprint, measured on CPU (AMD EPYC 9555, batch size 1, median of 20 runs).
62
+
63
+ ## Bearing fault (CWRU), input `(1, 2048, 1)`
64
+
65
+ | Format | Size on disk | Peak working RAM | Latency 1 core | Latency 4 core | Device class |
66
+ |---|---|---|---|---|---|
67
+ | fp32 | 0.53 MB | 0 MB | 2.358 ms | 2.955 ms | Microcontroller (STM32H7 / ESP32-S3) class |
68
+
69
+ ## Remaining useful life (C-MAPSS FD001), input `(1, 30, 14)`
70
+
71
+ | Format | Size on disk | Peak working RAM | Latency 1 core | Latency 4 core | Device class |
72
+ |---|---|---|---|---|---|
73
+ | fp32 | 0.5 MB | 0 MB | 0.405 ms | 0.722 ms | Microcontroller (STM32H7 / ESP32-S3) class |
74
+
75
+
76
+ int8 here compresses about 2.5 to 2.8x rather than the full 4x, because the learned Fourier
77
+ filters are kept in fp32 and only the linear layers are quantized. We expect that
78
+ quantizing the spectral filters hurts accuracy for little size gain at this scale.
79
+
80
+ # Accuracy
81
+
82
+ Numbers below are from our own training runs on the public datasets, on CPU. The "published
83
+ range" column is the typical range reported in the literature for the same protocol, given for
84
+ context, not as a controlled head to head.
85
+
86
+ ## Bearing fault classification (CWRU, 12 kHz drive end)
87
+
88
+ Protocol: 10 class problem (healthy plus inner race, ball, and outer race faults at three
89
+ defect diameters), all four motor loads pooled, raw vibration windows of 2048 samples with
90
+ 50 percent overlap, random 75/25 train/test split, per signal normalization. This is the
91
+ common CWRU window split protocol.
92
+
93
+ | Model | Metric | This model | Published range | Source |
94
+ |---|---|---|---|---|
95
+ | FELA-PdM (pure FNO) | test accuracy | 100.0% | 98 to 100% | measured (ours) |
96
+ | FELA-PdM (FNO + GLA) | test accuracy | 100.0% | 98 to 100% | measured (ours) |
97
+
98
+ The window split CWRU benchmark is close to saturated in the literature; strong models
99
+ routinely report 99 to 100 percent. FELA-PdM reaches the ceiling with a 132.6 thousand
100
+ parameter model. This protocol is known to be optimistic, because windows from the same
101
+ recording can land in both the train and the test set. Harder cross load and
102
+ cross fault size protocols were not run and are listed under Limitations.
103
+
104
+ ## Remaining useful life (NASA C-MAPSS turbofan)
105
+
106
+ Protocol: 14 informative sensors, min max normalized on the training set, sliding window of
107
+ 30 cycles, piecewise linear remaining useful life target capped at 125 cycles (the common
108
+ Heimes convention). Metric is RMSE in cycles on the official test set (one prediction per
109
+ test engine at its last available cycle), and the NASA PHM08 asymmetric score (lower is
110
+ better, late predictions penalized more).
111
+
112
+ | Subset | Metric | FELA-PdM RMSE | FELA-PdM score | Published RMSE range | Source |
113
+ |---|---|---|---|---|---|
114
+ | FD001 | RMSE / PHM08 score | 11.16 | 192 | 11 to 18 (CNN ~18.4, LSTM ~16.1, recent transformers ~11 to 13) | measured (ours) |
115
+ | FD002 | RMSE / PHM08 score | 19.64 | 2041 | 17 to 24 | measured (ours) |
116
+ | FD003 | RMSE / PHM08 score | 11.68 | 357 | 12 to 17 | measured (ours) |
117
+ | FD004 | RMSE / PHM08 score | 19.45 | 2217 | 19 to 25 | measured (ours) |
118
+
119
+ FD001 (single operating condition, single fault mode) is the canonical benchmark. FELA-PdM
120
+ reaches 11.16 RMSE, at the strong end of the published range and ahead of the classic CNN and
121
+ LSTM baselines, with a 124.5 thousand parameter model. FD003 (single condition) matches that
122
+ strong result. FD002 and FD004 (six operating conditions) are harder; those numbers sit inside
123
+ the published band rather than ahead of it. The FD002 to FD004 numbers were measured with the
124
+ same recipe as FD001 (pure FNO, 40 epochs, seed 0). All four C-MAPSS heads (FD001 to FD004) ship
125
+ as separate safetensors files, so every row above loads and reproduces from the shipped weights.
126
+
127
+ # How to run it
128
+
129
+ See `quickstart/` for a runnable example. The short version:
130
+
131
+ ```python
132
+ from modeling import load_model
133
+ # a directory holding <variant>.safetensors + config.json (or a Hugging Face repo id):
134
+ m = load_model("/path/to/weights_dir", variant="cmapss_FD001")
135
+ window = ... # (1, 30 cycles, 14 sensors); see modeling.preprocess_cmapss
136
+ remaining_cycles = m.predict(window) # remaining useful life estimate
137
+ ```
138
+
139
+ Pass `variant="cwru"` instead to load the bearing fault head, or `variant="cmapss_FD002"` (through
140
+ `FD004`) for the other C-MAPSS subsets. The weights ship one safetensors file per head
141
+ (`cmapss_FD001.safetensors` through `cmapss_FD004.safetensors`, and `cwru.safetensors`) beside
142
+ `config.json`. For an interactive playground, see the Hugging Face Space in `space/`.
143
+
144
+ ## Formats
145
+
146
+ - fp32: reference and CPU.
147
+ - int8: on device deployment format (AVX512-VNNI on x86, NEON dot product on ARM, and the
148
+ only realistic format on a microcontroller). About 0.21 MB (bearing) and 0.18 MB (RUL).
149
+ - bf16: server and GPU inference only; most commodity ARM and microcontroller CPUs lack
150
+ native bf16, so it is not the on device format.
151
+
152
+ # Training data
153
+
154
+ - CWRU bearing dataset (Case Western Reserve University Bearing Data Center): 12 kHz
155
+ drive end vibration recordings, four motor loads, used for the fault classifier. Public
156
+ research dataset.
157
+ - NASA C-MAPSS turbofan degradation simulation (Saxena et al. 2008), subsets FD001 to FD004,
158
+ used for the remaining useful life regressor. Public NASA dataset.
159
+ - MIMII (Purohit et al. 2019), machine sound, valve 6 dB subset: loader implemented, acoustic
160
+ head not shipped and not measured.
161
+
162
+ ## Training data, splits and licensing
163
+
164
+ The training and evaluation splits are defined in `train.py` in this repo, which covers all five
165
+ trained variants (C-MAPSS FD001 to FD004 plus CWRU). Both loaders and the exact split boundaries
166
+ are reproduced there, and a `--smoke` flag rebuilds each split, asserts the audited window count,
167
+ and exits before training.
168
+
169
+ ### CWRU bearing fault (classifier)
170
+
171
+ - Dataset: Case Western Reserve University Bearing Data Center, 12 kHz Drive End (DE_time)
172
+ vibration recordings. Version: the standard 40 file, 10 class, four motor load collection
173
+ (Normal plus inner race, ball, and outer race faults at 0.007 / 0.014 / 0.021 inch).
174
+ - Source: https://engineering.case.edu/bearingdatacenter
175
+ - Split: sliding windows of 2048 samples, stride 1024, per signal z normalization, all four
176
+ loads pooled; random 75/25 train/test split, seed 0. Total 5886 windows to 4415 train /
177
+ 1471 test, 10 classes. Split defined in train.py line 106 (the assertion `len(x) == 5886`
178
+ after `cwru_split`).
179
+ - License: NO explicit license is published by CWRU for this data. It is widely used and freely
180
+ downloadable, but the Bearing Data Center pages and the CWRU site wide legal notice grant no
181
+ reuse or commercial use rights.
182
+ - Commercial verdict: UNCLEAR / UNSTATED, no license grant. For commercial use, obtain
183
+ written permission from the Case School of Engineering. Third party mirrors (Kaggle, Zenodo)
184
+ do not establish CWRU's terms.
185
+
186
+ ### NASA C-MAPSS turbofan (remaining useful life regressor, FD001 to FD004)
187
+
188
+ - Dataset: NASA C-MAPSS Turbofan Engine Degradation Simulation Data Set (Saxena & Goebel 2008),
189
+ subsets FD001 to FD004, from the NASA Prognostics Center of Excellence (PCoE) data repository.
190
+ - Source: https://www.nasa.gov/intelligent-systems-division/discovery-and-systems-health/pcoe/pcoe-data-set-repository/
191
+ - Split: 14 informative sensors, min max normalized on the training set, sliding window of 30
192
+ cycles, piecewise linear RUL capped at 125 (Heimes convention). Train = all overlapping
193
+ 30 cycle windows per engine; test = the last window per engine scored against the official
194
+ provided RUL truth (NASA test protocol). FD001 train has 17731 windows (test 100 engines).
195
+ Split defined in train.py line 135 (the assertion `len(xtr) == 17731` for FD001 after
196
+ `load_cmapss`). Metric: RMSE and NASA PHM08 asymmetric score.
197
+ - License: no explicit license line on the PCoE repository. The data is a NASA authored
198
+ simulation (a US Government work), which under 17 U.S.C. section 105 is not protected
199
+ by US copyright and may be used, including commercially, without permission. Attribution to
200
+ NASA / Saxena & Goebel (2008) is requested.
201
+ - Commercial verdict: ALLOWED (US Government public domain work; attribution requested). Note:
202
+ US only public domain status; outside the US it is not guaranteed.
203
+
204
+ ### MIMII (acoustic head, not shipped)
205
+
206
+ - Dataset: MIMII (Purohit et al. 2019), valve 6 dB subset. Loader implemented; acoustic head
207
+ not shipped and not measured, so no split or license verdict is claimed for a released model
208
+ here.
209
+
210
+ ## Loading with standard tooling
211
+
212
+ The repo ships `config.json` (architecture hyperparameters for all five heads) and a
213
+ self contained `modeling.py` with a `load_model` / `from_pretrained` entry point. A few lines
214
+ load the model from a Hugging Face repo, a local directory, or a checkpoint:
215
+
216
+ ```python
217
+ from huggingface_hub import hf_hub_download
218
+ from modeling import load_model
219
+ # from a local dir holding model.safetensors + config.json:
220
+ m = load_model("/path/to/weights_dir", variant="cmapss_FD001")
221
+ # or straight from a HF repo id (downloads config.json + model.safetensors):
222
+ m = load_model("lowdown-labs/fela-pdm", variant="cwru")
223
+ ```
224
+
225
+ The weights are shipped one safetensors file per head (`cmapss_FD001.safetensors` through
226
+ `cmapss_FD004.safetensors`, and `cwru.safetensors`; not pickle); pass `variant=` to pick the
227
+ head. The preprocessing the model
228
+ expects, and input validation that fails clearly on the wrong shape or channel count, are in
229
+ `modeling.py` (`preprocess_cwru`, `preprocess_cmapss`, `validate_window`).
230
+
231
+ ## Serving artifacts
232
+
233
+ - `cmapss_FD001.safetensors` through `cmapss_FD004.safetensors` and `cwru.safetensors`, plus
234
+ `config.json`, for the safetensors load path (fp32).
235
+ - `verify.py` runs a fixed sample input and checks the output shape and a verification value.
236
+
237
+ For serving at scale, use the separate CPU native FELA server (https://github.com/Lowdown-Labs/fela_server). It
238
+ runs this model on CPU with no GPU required. The quickstart in this repo is the minimal
239
+ single process path; the FELA server is the production serving path. On a microcontroller or
240
+ Pi the deploy path is an ONNX or TFLite export of the model.
241
+
242
+ # Citations and licenses
243
+
244
+ This section consolidates the formal references and the direct links to the real license
245
+ text for every dataset and method used, verified from source.
246
+
247
+ ## Datasets
248
+
249
+ - **NASA C-MAPSS Turbofan Engine Degradation Simulation Data Set** (FD001 to FD004): the
250
+ remaining useful life regressor.
251
+ - Reference: Saxena, A., Goebel, K., Simon, D., & Eklund, N. (2008). Damage propagation
252
+ modeling for aircraft engine run-to-failure simulation. *International Conference on
253
+ Prognostics and Health Management (PHM08)*, 1 to 9.
254
+ DOI: [10.1109/PHM.2008.4711414](https://doi.org/10.1109/PHM.2008.4711414)
255
+ - Data: NASA Prognostics Center of Excellence (PCoE) data repository,
256
+ [NASA PCoE data set repository](https://www.nasa.gov/intelligent-systems-division/discovery-and-systems-health/pcoe/pcoe-data-set-repository/).
257
+ - **License: NASA data policy, US Government work, PUBLIC DOMAIN.** As a NASA authored
258
+ simulation, the data is a US Government work and under
259
+ [17 U.S.C. § 105](https://www.copyright.gov/title17/92chap1.html#105) is not protected by US
260
+ copyright; NASA's open data terms permit use, including commercial use, without permission.
261
+ See NASA's data usage guidelines:
262
+ [nasa.gov/nasa-open-data-and-usage-guidelines](https://www.nasa.gov/nasa-open-data-and-usage-guidelines/).
263
+ Attribution to NASA / Saxena & Goebel (2008) is requested. Public domain status is US only;
264
+ outside the US it is not guaranteed.
265
+ - **CWRU bearing dataset (Case Western Reserve University Bearing Data Center)**: 12 kHz
266
+ drive end vibration recordings, the bearing fault classifier.
267
+ - Data / use terms: [Case Western Reserve University Bearing Data Center](https://engineering.case.edu/bearingdatacenter),
268
+ which references the CWRU site wide legal notice:
269
+ [case.edu/utilities/privacy-legal](https://case.edu/utilities/privacy-legal).
270
+ - **License: NO explicit license or use terms grant is published by CWRU.** The data is freely
271
+ downloadable and widely used, but the Bearing Data Center pages and the CWRU legal notice grant
272
+ no reuse or commercial use rights. Commercial verdict UNCLEAR/UNSTATED; for commercial use,
273
+ obtain written permission from the Case School of Engineering. Third party mirrors (Kaggle,
274
+ Zenodo) do not establish CWRU's terms.
275
+ - **MIMII** (acoustic head: loader only, no weights shipped, no released model license claimed).
276
+ Purohit, H., Tanabe, R., Ichige, K., et al. (2019). MIMII Dataset: Sound Dataset for
277
+ Malfunctioning Industrial Machine Investigation and Inspection. *DCASE Workshop*.
278
+ [arXiv:1909.09347](https://arxiv.org/abs/1909.09347) (dataset is CC BY-SA 4.0 on Zenodo, cited
279
+ here for completeness only).
280
+
281
+ ## Methods and code
282
+
283
+ - **Fourier Neural Operator (FNO)**: the sequence mixer at the core of both heads.
284
+ Li, Z., et al. (2021). Fourier Neural Operator for Parametric Partial Differential Equations.
285
+ *ICLR*. [arXiv:2010.08895](https://arxiv.org/abs/2010.08895)
286
+ - **Gated Linear Attention (GLA)**: the optional gated recall mixer in the FNO+GLA variant
287
+ (`gla_chunk` in `config.json` / `modeling.py`). Yang, S., Wang, B., Shen, Y., Panda, R., & Kim, Y.
288
+ (2024). Gated Linear Attention Transformers with Hardware-Efficient Training.
289
+ [arXiv:2312.06635](https://arxiv.org/abs/2312.06635)
290
+ - **PyTorch**: training and inference framework. Paszke, A., et al. (2019). *NeurIPS*.
291
+ [arXiv:1912.01703](https://arxiv.org/abs/1912.01703)
292
+ - **ONNX Runtime / TFLite**: the on device export and runtime path (opset 17).
293
+ [onnxruntime.ai](https://onnxruntime.ai/),
294
+ [ai.google.dev/edge/litert](https://ai.google.dev/edge/litert).
295
+
296
+ The deployable default is the pure FNO head; the FNO+GLA variant is the one that additionally
297
+ uses Gated Linear Attention. Landmark Attention and Gated DeltaNet are not used in this model.
298
+
299
+ # Intended use, limitations, and safety
300
+
301
+ What it is for: on device predictive maintenance running on a PLC, a sensor gateway, or an
302
+ industrial IoT node, with no dependence on the cloud. Typical buyers are equipment makers who
303
+ sell machines with downtime guarantees, and plants that cannot or will not stream raw
304
+ vibration data off site.
305
+
306
+ What it is not for: this is not a safety critical controller and not a substitute for a
307
+ certified protection system. The remaining useful life number is a planning aid. Do not use it
308
+ as the sole basis for a safety critical decision (for example deciding a machine is safe to
309
+ keep running) without independent validation against your own field data and your existing
310
+ condition monitoring practice.
311
+
312
+ Privacy: the model runs on the device next to the sensor. Raw vibration and sensor data do not
313
+ have to leave the device, which is the point for plants that cannot send data off site.
314
+
315
+ Evaluated conditions and known failure modes:
316
+
317
+ - The CWRU window split protocol is optimistic: windows from one recording can appear in both
318
+ train and test, so 100 percent accuracy reflects an easy protocol, not a solved problem.
319
+ Cross load and cross fault size generalization (train on one motor load, test on another) is
320
+ the honest next test and is not yet reported.
321
+ - The C-MAPSS multi condition subsets FD002 and FD004 are not ahead of the literature; they sit
322
+ inside the published band. FD001 and FD003 (single condition) are the strong results.
323
+ - Remaining useful life is only as good as the run to failure data it was trained on. C-MAPSS
324
+ is simulated. Real machines fail in ways the training distribution may not cover.
325
+ - The MIMII acoustic head is not shipped; the path exists but the AUC is not measured.
326
+ - Quantization was dynamic int8 on linear layers only. A true microcontroller deployment needs
327
+ a fixed point FFT (for example CMSIS-DSP on Cortex-M) and on target validation, which is not
328
+ done here. Latency and size were measured on an x86 server CPU; the microcontroller claim is
329
+ the size plus compute envelope, run there via the ONNX or TFLite export.
330
+ - No real world field data was used. All benchmarks are public research datasets.
331
+
332
+ # How to cite
333
+
334
+ ```bibtex
335
+ @misc{lowdownlabs_felapdm,
336
+ title = {FELA-PdM: on-device Fourier Neural Operator models for predictive maintenance},
337
+ author = {Lowdown Labs},
338
+ year = {2026},
339
+ note = {Model card}
340
+ }
341
+ ```
342
+
343
+ You must also cite the datasets used:
344
+
345
+ - Saxena, A., Goebel, K., Simon, D., Eklund, N. (2008). Damage propagation modeling for
346
+ aircraft engine run-to-failure simulation (C-MAPSS / NASA turbofan). International
347
+ Conference on Prognostics and Health Management.
348
+ - Case Western Reserve University Bearing Data Center (CWRU bearing dataset).
349
+ - Purohit, H., Tanabe, R., Ichige, K., et al. (2019). MIMII Dataset: Sound Dataset for
350
+ Malfunctioning Industrial Machine Investigation and Inspection. DCASE Workshop.
351
+
352
+ # Acknowledgements and references
353
+
354
+ - C-MAPSS / NASA turbofan: Saxena et al. (2008).
355
+ - CWRU bearing dataset: Case Western Reserve University Bearing Data Center.
356
+ - MIMII: Purohit et al. (2019), DCASE.
357
+ - Fourier Neural Operator: Li, Z., Kovachki, N., Azizzadenesheli, K., et al. (2021). Fourier
358
+ Neural Operator for Parametric Partial Differential Equations. ICLR.
359
+ - PyTorch: Paszke et al. (2019), NeurIPS.
360
+
361
+ # Model family
362
+
363
+ This is part of the FELA family from Lowdown Labs: one FNO architecture across many
364
+ modalities, all CPU native and subquadratic. This repo is published as
365
+ `lowdown-labs/fela-pdm`. The sibling repos are:
366
+
367
+ - `lowdown-labs/fela-genomics`: DNA sequence classification.
368
+ - `lowdown-labs/fela-pdm` (this repo): rotating machinery and turbofan health.
369
+ - `lowdown-labs/fela-power-grid`: probabilistic solar and wind power forecasting.
370
+ - `lowdown-labs/fela-video`: video moment retrieval and temporal grounding.
371
+ - `lowdown-labs/fela-streaming-asr`: streaming CPU speech recognition.
372
+
373
+ These are grouped under the FELA Collection on Hugging Face. The models are independently
374
+ trained per modality and do not share weights, so none carries a `base_model` link.
375
+
376
+ # License
377
+
378
+ 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.
cmapss_FD001.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a9b625af024097d37f50d381bab3b6106363798746487538077787ac1d956ccc
3
+ size 500788
cmapss_FD002.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:02367e2a5905d58ae075db3f67f19b49fb3fa720057d43c280619e9a1832093e
3
+ size 500788
cmapss_FD003.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f65c4517d7e645c21bd4baed01ade87dfdaffe0492d56835f82f416d34b313f0
3
+ size 500788
cmapss_FD004.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c3705ce4f528830dcfdfceb897f94a48e2e11e4fdab4d1db8f739047af08f91c
3
+ size 500788
config.json ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "fela-pdm",
3
+ "library_name": "pytorch",
4
+ "architectures": [
5
+ "FelaPdmModel"
6
+ ],
7
+ "auto_map": {
8
+ "AutoConfig": "configuration_pdm.FelaPdmConfig",
9
+ "AutoModel": "modeling_pdm.FelaPdmModel"
10
+ },
11
+ "default_variant": "cmapss_FD001",
12
+ "note": "Five trained heads ship, one safetensors file per head (cmapss_FD001, cmapss_FD002, cmapss_FD003, cmapss_FD004, cwru). Pick variants[<name>] to match the head; load_model(dir, variant=<name>) resolves <name>.safetensors.",
13
+ "variants": {
14
+ "cmapss_FD001": {
15
+ "task": "rul",
16
+ "in_channels": 14,
17
+ "patch": 1,
18
+ "n_embd": 64,
19
+ "n_layer": 4,
20
+ "n_head": 4,
21
+ "fno_modes": 32,
22
+ "gla_chunk": 32,
23
+ "ffn_hidden": 128,
24
+ "dropout": 0.0,
25
+ "use_gdn": false,
26
+ "gdn_every": 4,
27
+ "n_classes": 0,
28
+ "rul_head": true,
29
+ "seq_len": 30,
30
+ "rul_cap": 125,
31
+ "input_shape": [1, 30, 14],
32
+ "input_desc": "30 cycles of 14 informative C-MAPSS sensors, min max normalized on training stats"
33
+ },
34
+ "cmapss_FD002": {
35
+ "task": "rul",
36
+ "in_channels": 14,
37
+ "patch": 1,
38
+ "n_embd": 64,
39
+ "n_layer": 4,
40
+ "n_head": 4,
41
+ "fno_modes": 32,
42
+ "gla_chunk": 32,
43
+ "ffn_hidden": 128,
44
+ "dropout": 0.0,
45
+ "use_gdn": false,
46
+ "gdn_every": 4,
47
+ "n_classes": 0,
48
+ "rul_head": true,
49
+ "seq_len": 30,
50
+ "rul_cap": 125,
51
+ "input_shape": [1, 30, 14],
52
+ "input_desc": "30 cycles of 14 informative C-MAPSS sensors, min max normalized on training stats"
53
+ },
54
+ "cmapss_FD003": {
55
+ "task": "rul",
56
+ "in_channels": 14,
57
+ "patch": 1,
58
+ "n_embd": 64,
59
+ "n_layer": 4,
60
+ "n_head": 4,
61
+ "fno_modes": 32,
62
+ "gla_chunk": 32,
63
+ "ffn_hidden": 128,
64
+ "dropout": 0.0,
65
+ "use_gdn": false,
66
+ "gdn_every": 4,
67
+ "n_classes": 0,
68
+ "rul_head": true,
69
+ "seq_len": 30,
70
+ "rul_cap": 125,
71
+ "input_shape": [1, 30, 14],
72
+ "input_desc": "30 cycles of 14 informative C-MAPSS sensors, min max normalized on training stats"
73
+ },
74
+ "cmapss_FD004": {
75
+ "task": "rul",
76
+ "in_channels": 14,
77
+ "patch": 1,
78
+ "n_embd": 64,
79
+ "n_layer": 4,
80
+ "n_head": 4,
81
+ "fno_modes": 32,
82
+ "gla_chunk": 32,
83
+ "ffn_hidden": 128,
84
+ "dropout": 0.0,
85
+ "use_gdn": false,
86
+ "gdn_every": 4,
87
+ "n_classes": 0,
88
+ "rul_head": true,
89
+ "seq_len": 30,
90
+ "rul_cap": 125,
91
+ "input_shape": [1, 30, 14],
92
+ "input_desc": "30 cycles of 14 informative C-MAPSS sensors, min max normalized on training stats"
93
+ },
94
+ "cwru": {
95
+ "task": "cls",
96
+ "in_channels": 1,
97
+ "patch": 4,
98
+ "n_embd": 64,
99
+ "n_layer": 4,
100
+ "n_head": 4,
101
+ "fno_modes": 64,
102
+ "gla_chunk": 32,
103
+ "ffn_hidden": 128,
104
+ "dropout": 0.0,
105
+ "use_gdn": false,
106
+ "gdn_every": 4,
107
+ "n_classes": 10,
108
+ "rul_head": false,
109
+ "seq_len": 2048,
110
+ "input_shape": [1, 2048, 1],
111
+ "input_desc": "2048 raw vibration samples at 12 kHz, single channel, per signal standardized"
112
+ }
113
+ }
114
+ }
configuration_pdm.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import PretrainedConfig
2
+
3
+
4
+ class FelaPdmConfig(PretrainedConfig):
5
+ model_type = "fela-pdm"
6
+
7
+ def __init__(
8
+ self,
9
+ variant="cmapss_FD001",
10
+ task="rul",
11
+ in_channels=14,
12
+ patch=1,
13
+ n_embd=64,
14
+ n_layer=4,
15
+ n_head=4,
16
+ fno_modes=32,
17
+ gla_chunk=32,
18
+ ffn_hidden=128,
19
+ dropout=0.0,
20
+ use_gdn=False,
21
+ gdn_every=4,
22
+ n_classes=0,
23
+ rul_head=True,
24
+ seq_len=30,
25
+ default_variant=None,
26
+ variants=None,
27
+ **kwargs,
28
+ ):
29
+ if isinstance(variants, dict):
30
+ name = variant or default_variant
31
+ if name in variants:
32
+ v = variants[name]
33
+ task = v.get("task", task)
34
+ in_channels = v.get("in_channels", in_channels)
35
+ patch = v.get("patch", patch)
36
+ n_embd = v.get("n_embd", n_embd)
37
+ n_layer = v.get("n_layer", n_layer)
38
+ n_head = v.get("n_head", n_head)
39
+ fno_modes = v.get("fno_modes", fno_modes)
40
+ gla_chunk = v.get("gla_chunk", gla_chunk)
41
+ ffn_hidden = v.get("ffn_hidden", ffn_hidden)
42
+ dropout = v.get("dropout", dropout)
43
+ use_gdn = v.get("use_gdn", use_gdn)
44
+ gdn_every = v.get("gdn_every", gdn_every)
45
+ n_classes = v.get("n_classes", n_classes)
46
+ rul_head = v.get("rul_head", rul_head)
47
+ seq_len = v.get("seq_len", seq_len)
48
+ self.variant = variant
49
+ self.task = task
50
+ self.in_channels = in_channels
51
+ self.patch = patch
52
+ self.n_embd = n_embd
53
+ self.n_layer = n_layer
54
+ self.n_head = n_head
55
+ self.fno_modes = fno_modes
56
+ self.gla_chunk = gla_chunk
57
+ self.ffn_hidden = ffn_hidden
58
+ self.dropout = dropout
59
+ self.use_gdn = use_gdn
60
+ self.gdn_every = gdn_every
61
+ self.n_classes = n_classes
62
+ self.rul_head = rul_head
63
+ self.seq_len = seq_len
64
+ if default_variant is not None:
65
+ self.default_variant = default_variant
66
+ if variants is not None:
67
+ self.variants = variants
68
+ super().__init__(**kwargs)
cwru.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aa5a9d9972f3c6895cc4a7fcf37cb3227154ef75bb9626c4a72047867a665509
3
+ size 533344
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a9b625af024097d37f50d381bab3b6106363798746487538077787ac1d956ccc
3
+ size 500788
modeling.py ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ import json
3
+ import os
4
+ from dataclasses import dataclass, asdict
5
+ import torch
6
+ import torch.nn as nn
7
+ import torch.nn.functional as F
8
+
9
+
10
+ @dataclass
11
+ class PDMConfig:
12
+ in_channels: int = 1
13
+ patch: int = 1
14
+ n_embd: int = 64
15
+ n_layer: int = 4
16
+ n_head: int = 4
17
+ fno_modes: int = 64
18
+ gla_chunk: int = 32
19
+ ffn_hidden: int = 128
20
+ dropout: float = 0.0
21
+ use_gdn: bool = False
22
+ gdn_every: int = 4
23
+ n_classes: int = 0
24
+ rul_head: bool = False
25
+ seq_len: int = 2048
26
+
27
+ def __post_init__(self):
28
+ assert self.n_embd % self.n_head == 0
29
+
30
+
31
+ class FNOSeqMixer(nn.Module):
32
+ def __init__(self, cfg: PDMConfig):
33
+ super().__init__()
34
+ self.M = cfg.fno_modes
35
+ self.filter_td = nn.Parameter(torch.empty(cfg.n_embd, cfg.fno_modes))
36
+ self.out_scale = nn.Linear(cfg.n_embd, cfg.n_embd, bias=False)
37
+ nn.init.normal_(self.filter_td, std=0.02)
38
+
39
+ def forward(self, x):
40
+ B, T, C = x.shape
41
+ n_use = min(self.M, T)
42
+ h = self.filter_td.new_zeros(2 * T, C)
43
+ h[:n_use] = self.filter_td[:, :n_use].T
44
+ xp = F.pad(x, (0, 0, 0, T))
45
+ Y = torch.fft.rfft(xp, dim=1) * torch.fft.rfft(h, dim=0).unsqueeze(0)
46
+ return self.out_scale(torch.fft.irfft(Y, n=2 * T, dim=1)[:, :T])
47
+
48
+
49
+ class SwiGLU(nn.Module):
50
+ def __init__(self, cfg: PDMConfig):
51
+ super().__init__()
52
+ d, hd = (cfg.n_embd, cfg.ffn_hidden)
53
+ self.gate = nn.Linear(d, hd, bias=False)
54
+ self.up = nn.Linear(d, hd, bias=False)
55
+ self.down = nn.Linear(hd, d, bias=False)
56
+ self.drop = nn.Dropout(cfg.dropout)
57
+
58
+ def forward(self, x):
59
+ return self.drop(self.down(F.silu(self.gate(x)) * self.up(x)))
60
+
61
+
62
+ class PDMBlock(nn.Module):
63
+ def __init__(self, cfg: PDMConfig):
64
+ super().__init__()
65
+ self.mixer = FNOSeqMixer(cfg)
66
+ self.ffn = SwiGLU(cfg)
67
+ self.ln1 = nn.RMSNorm(cfg.n_embd)
68
+ self.ln2 = nn.RMSNorm(cfg.n_embd)
69
+
70
+ def forward(self, x):
71
+ x = x + self.mixer(self.ln1(x))
72
+ x = x + self.ffn(self.ln2(x))
73
+ return x
74
+
75
+
76
+ class FELAPDM(nn.Module):
77
+ def __init__(self, cfg: PDMConfig):
78
+ super().__init__()
79
+ self.cfg = cfg
80
+ self.patch = cfg.patch
81
+ self.embed = nn.Linear(cfg.in_channels * cfg.patch, cfg.n_embd)
82
+ self.blocks = nn.ModuleList([PDMBlock(cfg) for _ in range(cfg.n_layer)])
83
+ self.ln_out = nn.RMSNorm(cfg.n_embd)
84
+ if cfg.n_classes > 0:
85
+ self.cls_head = nn.Linear(cfg.n_embd, cfg.n_classes)
86
+ if cfg.rul_head:
87
+ self.rul_head = nn.Linear(cfg.n_embd, 1)
88
+
89
+ def _patchify(self, x):
90
+ B, T, Cin = x.shape
91
+ p = self.patch
92
+ if p > 1:
93
+ T2 = T // p * p
94
+ x = x[:, :T2].reshape(B, T2 // p, p * Cin)
95
+ return x
96
+
97
+ def _backbone(self, x):
98
+ x = self._patchify(x)
99
+ x = self.embed(x)
100
+ x = F.rms_norm(x, (x.size(-1),))
101
+ for blk in self.blocks:
102
+ x = blk(x)
103
+ return self.ln_out(x)
104
+
105
+ def forward(self, x, task: str = None):
106
+ task = task or ("rul" if self.cfg.rul_head else "cls")
107
+ h = self._backbone(x)
108
+ if task == "cls":
109
+ return self.cls_head(h.mean(dim=1))
110
+ if task == "rul":
111
+ return self.rul_head(h[:, -1, :]).squeeze(-1)
112
+ raise ValueError(task)
113
+
114
+ @torch.no_grad()
115
+ def predict(self, x, task: str = None):
116
+ self.eval()
117
+ validate_window(x, self.cfg)
118
+ out = self.forward(x, task=task)
119
+ if (task or ("rul" if self.cfg.rul_head else "cls")) == "cls":
120
+ probs = torch.softmax(out.float(), dim=-1)[0]
121
+ idx = int(probs.argmax())
122
+ return (idx, float(probs[idx]))
123
+ return float(out.reshape(-1)[0])
124
+
125
+
126
+ def validate_window(x: torch.Tensor, cfg: PDMConfig):
127
+ if x.dim() != 3:
128
+ raise ValueError(
129
+ f"expected a 3D tensor (batch, time, channels), got shape {tuple(x.shape)}"
130
+ )
131
+ if x.shape[-1] != cfg.in_channels:
132
+ raise ValueError(
133
+ f"expected {cfg.in_channels} channels in the last dimension, got {x.shape[-1]}. CWRU vibration is 1 channel; C-MAPSS is 14 sensors."
134
+ )
135
+ if x.shape[1] < cfg.patch:
136
+ raise ValueError(
137
+ f"time dimension {x.shape[1]} is shorter than the patch size {cfg.patch}"
138
+ )
139
+
140
+
141
+ def preprocess_cwru(samples, expected_len: int = 2048) -> torch.Tensor:
142
+ t = torch.as_tensor(samples, dtype=torch.float32).reshape(-1)
143
+ if t.numel() != expected_len:
144
+ raise ValueError(
145
+ f"CWRU window must be {expected_len} samples (12 kHz), got {t.numel()}"
146
+ )
147
+ t = (t - t.mean()) / (t.std() + 1e-06)
148
+ return t.reshape(1, expected_len, 1)
149
+
150
+
151
+ def preprocess_cmapss(cycles, sensor_min, sensor_max, window: int = 30) -> torch.Tensor:
152
+ t = torch.as_tensor(cycles, dtype=torch.float32)
153
+ if t.dim() != 2 or t.shape[1] != 14:
154
+ raise ValueError(f"C-MAPSS input must be (window, 14), got {tuple(t.shape)}")
155
+ if t.shape[0] != window:
156
+ raise ValueError(f"C-MAPSS window must be {window} cycles, got {t.shape[0]}")
157
+ lo = torch.as_tensor(sensor_min, dtype=torch.float32)
158
+ hi = torch.as_tensor(sensor_max, dtype=torch.float32)
159
+ t = (t - lo) / (hi - lo + 1e-06)
160
+ return t.reshape(1, window, 14)
161
+
162
+
163
+ def _load_state(path: str):
164
+ if path.endswith(".safetensors"):
165
+ from safetensors.torch import load_file
166
+
167
+ return load_file(path)
168
+ ck = torch.load(path, map_location="cpu", weights_only=False)
169
+ return ck["model"] if isinstance(ck, dict) and "model" in ck else ck
170
+
171
+
172
+ _CONFIG_FIELDS = set(PDMConfig.__dataclass_fields__.keys())
173
+
174
+
175
+ def _to_pdm_config(cfg_dict: dict, variant: str = None) -> PDMConfig:
176
+ if "variants" in cfg_dict:
177
+ variant = variant or cfg_dict.get("default_variant")
178
+ if variant not in cfg_dict["variants"]:
179
+ raise ValueError(
180
+ f"unknown variant {variant!r}; choose one of {list(cfg_dict['variants'])}"
181
+ )
182
+ cfg_dict = cfg_dict["variants"][variant]
183
+ return PDMConfig(**{k: v for k, v in cfg_dict.items() if k in _CONFIG_FIELDS})
184
+
185
+
186
+ def load_model(path_or_repo: str, config: dict = None, variant: str = None):
187
+ cfg_dict = config
188
+ weights_path = None
189
+ if os.path.isdir(path_or_repo):
190
+ cfg_dict = cfg_dict or _read_json(os.path.join(path_or_repo, "config.json"))
191
+ v = variant or (
192
+ cfg_dict.get("default_variant") if isinstance(cfg_dict, dict) else None
193
+ )
194
+ cand = os.path.join(path_or_repo, f"{v}.safetensors") if v else None
195
+ if cand and os.path.isfile(cand):
196
+ weights_path = cand
197
+ else:
198
+ weights_path = os.path.join(path_or_repo, "model.safetensors")
199
+ elif os.path.isfile(path_or_repo):
200
+ if path_or_repo.endswith(".safetensors"):
201
+ beside = os.path.join(os.path.dirname(path_or_repo), "config.json")
202
+ cfg_dict = cfg_dict or _read_json(beside)
203
+ weights_path = path_or_repo
204
+ else:
205
+ ck = torch.load(path_or_repo, map_location="cpu", weights_only=False)
206
+ cfg_dict = cfg_dict or ck["cfg"]
207
+ model = FELAPDM(_to_pdm_config(cfg_dict, variant))
208
+ model.load_state_dict(ck["model"])
209
+ model.eval()
210
+ return model
211
+ else:
212
+ from huggingface_hub import hf_hub_download
213
+
214
+ cfg_path = hf_hub_download(path_or_repo, "config.json")
215
+ cfg_dict = cfg_dict or _read_json(cfg_path)
216
+ v = variant or (
217
+ cfg_dict.get("default_variant") if isinstance(cfg_dict, dict) else None
218
+ )
219
+ try:
220
+ weights_path = (
221
+ hf_hub_download(path_or_repo, f"{v}.safetensors")
222
+ if v
223
+ else hf_hub_download(path_or_repo, "model.safetensors")
224
+ )
225
+ except Exception:
226
+ weights_path = hf_hub_download(path_or_repo, "model.safetensors")
227
+ model = FELAPDM(_to_pdm_config(cfg_dict, variant))
228
+ model.load_state_dict(_load_state(weights_path))
229
+ model.eval()
230
+ return model
231
+
232
+
233
+ from_pretrained = load_model
234
+
235
+
236
+ def _read_json(path: str) -> dict:
237
+ with open(path) as f:
238
+ return json.load(f)
modeling_pdm.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
5
+ from transformers import PreTrainedModel
6
+ from transformers.modeling_outputs import CausalLMOutput
7
+
8
+ from .configuration_pdm import FelaPdmConfig
9
+ from .modeling import FELAPDM, PDMConfig
10
+
11
+
12
+ class FelaPdmModel(PreTrainedModel):
13
+ config_class = FelaPdmConfig
14
+ base_model_prefix = "model"
15
+ main_input_name = "x"
16
+
17
+ def __init__(self, config):
18
+ super().__init__(config)
19
+ cfg = PDMConfig(
20
+ in_channels=config.in_channels,
21
+ patch=config.patch,
22
+ n_embd=config.n_embd,
23
+ n_layer=config.n_layer,
24
+ n_head=config.n_head,
25
+ fno_modes=config.fno_modes,
26
+ gla_chunk=config.gla_chunk,
27
+ ffn_hidden=config.ffn_hidden,
28
+ dropout=config.dropout,
29
+ use_gdn=config.use_gdn,
30
+ gdn_every=config.gdn_every,
31
+ n_classes=config.n_classes,
32
+ rul_head=config.rul_head,
33
+ seq_len=config.seq_len,
34
+ )
35
+ self.model = FELAPDM(cfg)
36
+ self.task = config.task
37
+ self.post_init()
38
+
39
+ def forward(self, x=None, input_values=None, task=None, **kwargs):
40
+ if x is None:
41
+ x = input_values
42
+ out = self.model(x, task=task or self.task)
43
+ return CausalLMOutput(logits=out)
quickstart/README.md ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Quickstart
2
+
3
+ Load a FELA-PdM head and run one real sensor window on CPU. Uses the self contained loader
4
+ in `../modeling.py`.
5
+
6
+ ## Steps
7
+
8
+ 1. Install the pinned requirements (CPU PyTorch):
9
+
10
+ ```bash
11
+ pip install -r requirements.txt
12
+ ```
13
+
14
+ 2. Get the weights. One safetensors file per head ships in this repo
15
+ (`cmapss_FD001.safetensors`, `cwru.safetensors`) beside `config.json`. Point at the repo
16
+ directory and pass `--variant` to pick the head:
17
+
18
+ ```bash
19
+ export FELA_PDM_WEIGHTS=/path/to/weights_dir # holds <variant>.safetensors + config.json
20
+ ```
21
+
22
+ 3. Run:
23
+
24
+ ```bash
25
+ python run.py --variant cmapss_FD001 # remaining useful life
26
+ python run.py --variant cwru # bearing fault class
27
+ ```
28
+
29
+ ## Few line load from Python
30
+
31
+ ```python
32
+ from modeling import load_model
33
+ m = load_model("/path/to/weights_dir", variant="cmapss_FD001") # dir, .pt, or HF repo id
34
+ rul = m.predict(window) # window: (1, 30, 14) sensor cycles
35
+ ```
36
+
37
+ The C-MAPSS RUL head expects 30 cycles of 14 sensors (min max normalized on the training
38
+ statistics). The CWRU head expects 2048 raw vibration samples (12 kHz, per signal
39
+ standardized). See `modeling.preprocess_cmapss` and `modeling.preprocess_cwru` for the exact
40
+ preprocessing, and `modeling.validate_window` for input validation.
quickstart/requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # Pinned for reproducible CPU inference. On a machine without CUDA, `pip install torch`
2
+ # pulls the matching CPU wheel. The quickstart runs from a clean venv with only these.
3
+ torch==2.8.0
4
+ numpy==2.1.3
5
+ safetensors==0.5.3
6
+ huggingface-hub==0.34.4
quickstart/run.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import os
3
+ import sys
4
+ import torch
5
+
6
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
7
+ from modeling import load_model, validate_window
8
+
9
+
10
+ def main():
11
+ ap = argparse.ArgumentParser()
12
+ ap.add_argument(
13
+ "--variant",
14
+ default="cmapss_FD001",
15
+ choices=[
16
+ "cmapss_FD001",
17
+ "cmapss_FD002",
18
+ "cmapss_FD003",
19
+ "cmapss_FD004",
20
+ "cwru",
21
+ ],
22
+ )
23
+ ap.add_argument(
24
+ "--weights",
25
+ default=os.environ.get("FELA_PDM_WEIGHTS", "."),
26
+ help="directory with <variant>.safetensors + config.json, or a .pt checkpoint path",
27
+ )
28
+ args = ap.parse_args()
29
+ src = args.weights
30
+ variant_file = os.path.join(src, f"{args.variant}.safetensors")
31
+ if os.path.isdir(src) and os.path.isfile(variant_file):
32
+ model = load_model(src, variant=args.variant)
33
+ elif os.path.isfile(src):
34
+ model = load_model(src, variant=args.variant)
35
+ else:
36
+ raise SystemExit(
37
+ f"No weights at {src}. Set FELA_PDM_WEIGHTS to a directory holding {args.variant}.safetensors and config.json (or pass a .pt checkpoint path). Weights are in lowdown-labs/FELA-pdm."
38
+ )
39
+ if args.variant.startswith("cmapss"):
40
+ window = torch.randn(1, 30, 14)
41
+ validate_window(window, model.cfg)
42
+ rul = model.predict(window, task="rul")
43
+ print(f"Variant: {args.variant}")
44
+ print(f"Input shape: {tuple(window.shape)} (30 cycles, 14 sensors)")
45
+ print(f"Estimated remaining useful life: {rul:.1f} cycles (capped at 125)")
46
+ else:
47
+ window = torch.randn(1, 2048, 1)
48
+ validate_window(window, model.cfg)
49
+ idx, prob = model.predict(window, task="cls")
50
+ print(f"Variant: {args.variant}")
51
+ print(f"Input shape: {tuple(window.shape)} (2048 vibration samples, 1 channel)")
52
+ print(f"Predicted fault class index: {idx} (probability {prob:.4f})")
53
+
54
+
55
+ if __name__ == "__main__":
56
+ main()
space/README.md ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: FELA-PdM playground
3
+ colorFrom: yellow
4
+ colorTo: red
5
+ sdk: gradio
6
+ sdk_version: 4.44.0
7
+ app_file: app.py
8
+ pinned: false
9
+ license: other
10
+ ---
11
+
12
+ # FELA-PdM playground
13
+
14
+ Feed a sensor window and see the model's call. Two tabs: remaining useful life (C-MAPSS, 30
15
+ cycles of 14 sensors) and bearing fault (CWRU, 2048 vibration samples). Runs on CPU.
16
+
17
+ ## The demo data (and why it shows generalization)
18
+
19
+ The remaining useful life head was trained on C-MAPSS subset FD001 (a single operating
20
+ condition). The other subsets, FD002, FD003, and FD004, use different operating conditions and
21
+ fault modes and were not in this head's training set, so a window from those subsets is a real
22
+ out of distribution test. They are public NASA data from the Prognostics Data Repository
23
+ (Saxena et al. 2008); download a window and paste it into the RUL tab.
24
+
25
+ The bundled examples (the synthetic RUL window and the synthetic vibration window) are clearly
26
+ labeled as synthetic and illustrative. They let the Space load fast without a large download.
27
+ They are not real measurements and should not be read as benchmark results.
28
+
29
+ ## Weights
30
+
31
+ Set `FELA_PDM_WEIGHTS` (or a Space secret) to a directory holding the per head safetensors
32
+ (`cmapss_FD001.safetensors`, `cwru.safetensors`) and `config.json`. The weights are in the
33
+ Hugging Face repo `lowdown-labs/FELA-pdm`.
34
+
35
+ ## Note
36
+
37
+ For research and illustration only. This is not a safety critical controller. Do not act on the
38
+ remaining useful life number without independent validation against your own field data.
space/app.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import gradio as gr
4
+ import numpy as np
5
+ import torch
6
+
7
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
8
+ from modeling import load_model, validate_window
9
+
10
+ WEIGHTS = os.environ.get("FELA_PDM_WEIGHTS", ".")
11
+ MODELS = {}
12
+
13
+
14
+ def _try_load(variant):
15
+ try:
16
+ return load_model(WEIGHTS, variant=variant)
17
+ except Exception:
18
+ return None
19
+
20
+
21
+ for v in ("cmapss_FD001", "cwru"):
22
+ MODELS[v] = _try_load(v)
23
+ EXAMPLE_RUL = np.tile(np.linspace(0.3, 0.7, 30)[:, None], (1, 14))
24
+
25
+
26
+ def score_rul(text):
27
+ m = MODELS.get("cmapss_FD001")
28
+ if m is None:
29
+ return "RUL weights not found. Set FELA_PDM_WEIGHTS to a dir with cmapss_FD001.safetensors + config.json."
30
+ try:
31
+ rows = [r for r in text.strip().splitlines() if r.strip()]
32
+ arr = np.array(
33
+ [[float(x) for x in r.replace(",", " ").split()] for r in rows],
34
+ dtype=np.float32,
35
+ )
36
+ except Exception:
37
+ return "Could not parse. Provide 30 rows of 14 numbers (whitespace or comma separated)."
38
+ if arr.shape != (30, 14):
39
+ return f"Expected a (30, 14) window, got {arr.shape}."
40
+ x = torch.from_numpy(arr).reshape(1, 30, 14)
41
+ validate_window(x, m.cfg)
42
+ rul = m.predict(x, task="rul")
43
+ return f"estimated remaining useful life: {rul:.1f} cycles (capped at 125)"
44
+
45
+
46
+ def load_rul_example():
47
+ return "\n".join((" ".join((f"{v:.3f}" for v in row)) for row in EXAMPLE_RUL))
48
+
49
+
50
+ def score_cwru(text, use_synth):
51
+ m = MODELS.get("cwru")
52
+ if m is None:
53
+ return "Bearing weights not found. Set FELA_PDM_WEIGHTS to a dir with cwru.safetensors + config.json."
54
+ if use_synth or not text.strip():
55
+ t = np.linspace(0, 1, 2048)
56
+ sig = np.sin(2 * np.pi * 120 * t) + 0.2 * np.random.randn(2048)
57
+ else:
58
+ try:
59
+ sig = np.array(
60
+ [float(x) for x in text.replace(",", " ").split()], dtype=np.float32
61
+ )
62
+ except Exception:
63
+ return (
64
+ "Could not parse. Provide 2048 whitespace- or comma-separated samples."
65
+ )
66
+ if sig.size != 2048:
67
+ return f"Expected 2048 samples, got {sig.size}."
68
+ sig = (sig - sig.mean()) / (sig.std() + 1e-06)
69
+ x = torch.from_numpy(sig.astype(np.float32)).reshape(1, 2048, 1)
70
+ validate_window(x, m.cfg)
71
+ idx, prob = m.predict(x, task="cls")
72
+ return f"predicted fault class index: {idx} (probability {prob:.4f})"
73
+
74
+
75
+ with gr.Blocks(title="FELA-PdM playground") as demo:
76
+ gr.Markdown(
77
+ "# FELA-PdM playground\nOn-device predictive maintenance. Feed a sensor window and see the model's call. For research and illustration only, not a safety-critical controller; do not act on the remaining-useful-life number without independent validation."
78
+ )
79
+ with gr.Tab("Remaining useful life (C-MAPSS)"):
80
+ gr.Markdown(
81
+ "Paste 30 cycles of 14 normalized sensor values (30 rows, 14 numbers each). The FD001 head was not trained on FD002/FD003/FD004, so a window from those public NASA subsets is a real out-of-distribution test. The example below is synthetic and illustrative."
82
+ )
83
+ rul_in = gr.Textbox(label="sensor window (30 x 14)", lines=8)
84
+ rul_out = gr.Textbox(label="result")
85
+ with gr.Row():
86
+ gr.Button("Load synthetic example").click(load_rul_example, outputs=rul_in)
87
+ gr.Button("Score").click(score_rul, inputs=rul_in, outputs=rul_out)
88
+ with gr.Tab("Bearing fault (CWRU)"):
89
+ gr.Markdown(
90
+ "Paste 2048 raw vibration samples (12 kHz), or tick the box to score a synthetic illustrative window. The output is a fault-class index (10 classes)."
91
+ )
92
+ cwru_in = gr.Textbox(label="vibration window (2048 samples)", lines=4)
93
+ cwru_synth = gr.Checkbox(
94
+ label="use a synthetic illustrative window", value=True
95
+ )
96
+ cwru_out = gr.Textbox(label="result")
97
+ gr.Button("Score").click(
98
+ score_cwru, inputs=[cwru_in, cwru_synth], outputs=cwru_out
99
+ )
100
+ if __name__ == "__main__":
101
+ demo.launch()
space/requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ torch==2.8.0
2
+ numpy==2.1.3
3
+ safetensors==0.5.3
4
+ huggingface-hub==0.34.4
5
+ gradio==4.44.0
streaming/cmapss_FD001_fp16.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b605009813d95d5e4f4994e7dc878a70774bbce0ebbdb1ac1405f31b94a4b65b
3
+ size 251818
streaming/cmapss_FD002_fp16.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:16c15598bee3e12b1784c8fccbaeaa3b95fe90ed23154f3f737e5b26b5aabc28
3
+ size 251818
streaming/cmapss_FD003_fp16.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aec256a567108ee5a5b3aa4cdda5c94bfa674b1ec7e5f38c9c2c93e9868a2642
3
+ size 251818
streaming/cmapss_FD004_fp16.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c98243efa2b098460503f98081fb6c09025db71af5119a93b734db109060b95c
3
+ size 251818
streaming/cwru_fp16.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:35a2536bf6d03672e1c828d69e71bbdca935058a29e7e0aaad9ee705c9418add
3
+ size 268100
streaming/manifest.json ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model": "predictive-maintenance",
3
+ "format": "fp16-streaming",
4
+ "note": "load order is smallest-first for progressive/streaming load",
5
+ "files": [
6
+ {
7
+ "file": "cmapss_FD001_fp16.safetensors",
8
+ "source": "cmapss_FD001.safetensors",
9
+ "dtype": "fp16",
10
+ "bytes": 251818,
11
+ "approx_mb": 0.24
12
+ },
13
+ {
14
+ "file": "cmapss_FD002_fp16.safetensors",
15
+ "source": "cmapss_FD002.safetensors",
16
+ "dtype": "fp16",
17
+ "bytes": 251818,
18
+ "approx_mb": 0.24
19
+ },
20
+ {
21
+ "file": "cmapss_FD003_fp16.safetensors",
22
+ "source": "cmapss_FD003.safetensors",
23
+ "dtype": "fp16",
24
+ "bytes": 251818,
25
+ "approx_mb": 0.24
26
+ },
27
+ {
28
+ "file": "cmapss_FD004_fp16.safetensors",
29
+ "source": "cmapss_FD004.safetensors",
30
+ "dtype": "fp16",
31
+ "bytes": 251818,
32
+ "approx_mb": 0.24
33
+ },
34
+ {
35
+ "file": "model_fp16.safetensors",
36
+ "source": "model.safetensors",
37
+ "dtype": "fp16",
38
+ "bytes": 251818,
39
+ "approx_mb": 0.24
40
+ },
41
+ {
42
+ "file": "cwru_fp16.safetensors",
43
+ "source": "cwru.safetensors",
44
+ "dtype": "fp16",
45
+ "bytes": 268100,
46
+ "approx_mb": 0.256
47
+ }
48
+ ]
49
+ }
streaming/model_fp16.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b605009813d95d5e4f4994e7dc878a70774bbce0ebbdb1ac1405f31b94a4b65b
3
+ size 251818
train.py ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse, glob, os, time
2
+ import numpy as np, torch, torch.nn as nn, torch.nn.functional as F
3
+ import scipy.io as sio
4
+ from modeling import FELAPDM, PDMConfig
5
+
6
+ cwru_classes = [
7
+ "Normal",
8
+ "IR007",
9
+ "B007",
10
+ "OR007",
11
+ "IR014",
12
+ "B014",
13
+ "OR014",
14
+ "IR021",
15
+ "B021",
16
+ "OR021",
17
+ ]
18
+ cwru_idx = {c: i for i, c in enumerate(cwru_classes)}
19
+ sensor_cols = [6, 7, 8, 11, 12, 13, 15, 16, 17, 18, 19, 21, 24, 25]
20
+ rul_cap = 125
21
+
22
+
23
+ def cwru_label(f):
24
+ b = os.path.basename(f).split("_")[0]
25
+ if b in cwru_idx:
26
+ return cwru_idx[b]
27
+ raise ValueError(f)
28
+
29
+
30
+ def load_cwru(d, win=2048, stride=1024, loads=("0", "1", "2", "3"), seed=0):
31
+ rng = np.random.default_rng(seed)
32
+ xs, ys = ([], [])
33
+ for f in sorted(glob.glob(os.path.join(d, "*.mat"))):
34
+ load = os.path.basename(f).split("_")[1].replace(".mat", "")
35
+ if load not in loads:
36
+ continue
37
+ m = sio.loadmat(f)
38
+ de = [k for k in m if k.endswith("DE_time")]
39
+ if not de:
40
+ continue
41
+ sig = m[de[0]].ravel().astype(np.float32)
42
+ sig = (sig - sig.mean()) / (sig.std() + 1e-08)
43
+ lab = cwru_label(f)
44
+ for s in range(0, len(sig) - win + 1, stride):
45
+ xs.append(sig[s : s + win])
46
+ ys.append(lab)
47
+ x = np.stack(xs).astype(np.float32)[..., None]
48
+ y = np.array(ys, np.int64)
49
+ idx = rng.permutation(len(x))
50
+ return (x[idx], y[idx])
51
+
52
+
53
+ def cwru_split(x, y, test_frac=0.25, seed=0):
54
+ rng = np.random.default_rng(seed)
55
+ n = len(x)
56
+ perm = rng.permutation(n)
57
+ nte = int(n * test_frac)
58
+ te, tr = (perm[:nte], perm[nte:])
59
+ return (x[tr], y[tr], x[te], y[te])
60
+
61
+
62
+ def read_cmapss(p):
63
+ return np.loadtxt(p)
64
+
65
+
66
+ def load_cmapss(d, subset="FD001", win=30, cap=rul_cap):
67
+ train = read_cmapss(os.path.join(d, f"train_{subset}.txt"))
68
+ test = read_cmapss(os.path.join(d, f"test_{subset}.txt"))
69
+ truth = read_cmapss(os.path.join(d, f"RUL_{subset}.txt")).ravel()
70
+ s = sensor_cols
71
+ tr_s = train[:, s]
72
+ smin, smax = (tr_s.min(0), tr_s.max(0))
73
+ rng = smax - smin
74
+ rng[rng == 0] = 1.0
75
+
76
+ def norm(a):
77
+ return (a[:, s] - smin) / rng
78
+
79
+ xtr, ytr = ([], [])
80
+ for u in np.unique(train[:, 0]):
81
+ eng = train[train[:, 0] == u]
82
+ feats = norm(eng).astype(np.float32)
83
+ L = len(feats)
84
+ mc = eng[:, 1].max()
85
+ for e in range(win, L + 1):
86
+ xtr.append(feats[e - win : e])
87
+ ytr.append(min(mc - eng[e - 1, 1], cap))
88
+ if L < win:
89
+ pad = np.zeros((win - L, len(s)), np.float32)
90
+ xtr.append(np.concatenate([pad, feats], 0))
91
+ ytr.append(min(mc - eng[-1, 1], cap))
92
+ xtr = np.stack(xtr).astype(np.float32)
93
+ ytr = np.array(ytr, np.float32)
94
+ xte, yte = ([], [])
95
+ for i, u in enumerate(np.unique(test[:, 0])):
96
+ eng = test[test[:, 0] == u]
97
+ feats = norm(eng).astype(np.float32)
98
+ L = len(feats)
99
+ if L >= win:
100
+ w = feats[L - win :]
101
+ else:
102
+ pad = np.zeros((win - L, len(s)), np.float32)
103
+ w = np.concatenate([pad, feats], 0)
104
+ xte.append(w)
105
+ yte.append(min(truth[i], cap))
106
+ return (xtr, ytr, np.stack(xte).astype(np.float32), np.array(yte, np.float32))
107
+
108
+
109
+ def cmapss_score(yt, yp):
110
+ d = yp - yt
111
+ s = np.where(d < 0, np.exp(-d / 13.0) - 1.0, np.exp(d / 10.0) - 1.0)
112
+ return (float(np.sqrt(np.mean(d**2))), float(np.sum(s)))
113
+
114
+
115
+ def batches(x, y, bs, seed=0):
116
+ idx = np.arange(len(x))
117
+ np.random.default_rng(seed).shuffle(idx)
118
+ for i in range(0, len(x), bs):
119
+ j = idx[i : i + bs]
120
+ yield (x[j], y[j])
121
+
122
+
123
+ def cfg_cwru(**kw):
124
+ c = PDMConfig(
125
+ in_channels=1,
126
+ patch=4,
127
+ n_embd=64,
128
+ n_layer=4,
129
+ n_head=4,
130
+ fno_modes=64,
131
+ ffn_hidden=128,
132
+ n_classes=10,
133
+ seq_len=2048,
134
+ )
135
+ for k, v in kw.items():
136
+ setattr(c, k, v)
137
+ return c
138
+
139
+
140
+ def cfg_cmapss(**kw):
141
+ c = PDMConfig(
142
+ in_channels=14,
143
+ patch=1,
144
+ n_embd=64,
145
+ n_layer=4,
146
+ n_head=4,
147
+ fno_modes=32,
148
+ ffn_hidden=128,
149
+ rul_head=True,
150
+ seq_len=64,
151
+ )
152
+ for k, v in kw.items():
153
+ setattr(c, k, v)
154
+ return c
155
+
156
+
157
+ def train_cwru(a):
158
+ dev = torch.device(a.device)
159
+ x, y = load_cwru(a.data, win=2048, stride=a.stride, seed=a.seed)
160
+ xtr, ytr, xte, yte = cwru_split(x, y, test_frac=0.25, seed=a.seed)
161
+ assert len(x) == 5886
162
+ if a.smoke:
163
+ print(
164
+ f"Cwru total {len(x)} train {len(xtr)} test {len(xte)} classes {len(set(y.tolist()))}"
165
+ )
166
+ return
167
+ cfg = cfg_cwru(dropout=a.dropout)
168
+ m = FELAPDM(cfg).to(dev)
169
+ opt = torch.optim.AdamW(m.parameters(), lr=a.lr, weight_decay=0.0001)
170
+ sch = torch.optim.lr_scheduler.CosineAnnealingLR(opt, a.epochs)
171
+ xte_t = torch.from_numpy(xte).to(dev)
172
+ yte_t = torch.from_numpy(yte).to(dev)
173
+ best = 0.0
174
+ for ep in range(a.epochs):
175
+ m.train()
176
+ for xb, yb in batches(xtr, ytr, a.bs, seed=a.seed + ep):
177
+ xb = torch.from_numpy(xb).to(dev)
178
+ yb = torch.from_numpy(yb).to(dev)
179
+ loss = F.cross_entropy(m(xb, task="cls"), yb)
180
+ opt.zero_grad()
181
+ loss.backward()
182
+ nn.utils.clip_grad_norm_(m.parameters(), 1.0)
183
+ opt.step()
184
+ sch.step()
185
+ m.eval()
186
+ with torch.no_grad():
187
+ pr = torch.cat(
188
+ [
189
+ m(xte_t[i : i + 256], task="cls").argmax(-1)
190
+ for i in range(0, len(xte_t), 256)
191
+ ]
192
+ )
193
+ acc = (pr == yte_t).float().mean().item()
194
+ if acc > best:
195
+ best = acc
196
+ torch.save(
197
+ {"cfg": cfg.__dict__, "model": m.state_dict(), "classes": cwru_classes},
198
+ a.out,
199
+ )
200
+ print(f"Ep {ep:02d} acc {acc * 100:.2f} best {best * 100:.2f}")
201
+
202
+
203
+ def train_cmapss(a):
204
+ dev = torch.device(a.device)
205
+ xtr, ytr, xte, yte = load_cmapss(a.data, subset=a.subset, win=a.win)
206
+ if a.subset == "FD001":
207
+ assert len(xtr) == 17731
208
+ if a.smoke:
209
+ print(f"Cmapss {a.subset} train {xtr.shape} test {xte.shape}")
210
+ return
211
+ cfg = cfg_cmapss(dropout=a.dropout, seq_len=a.win)
212
+ m = FELAPDM(cfg).to(dev)
213
+ opt = torch.optim.AdamW(m.parameters(), lr=a.lr, weight_decay=0.0001)
214
+ sch = torch.optim.lr_scheduler.CosineAnnealingLR(opt, a.epochs)
215
+ cap = 125.0
216
+ xte_t = torch.from_numpy(xte).to(dev)
217
+ best = 1000000000.0
218
+ for ep in range(a.epochs):
219
+ m.train()
220
+ for xb, yb in batches(xtr, ytr / cap, a.bs, seed=a.seed + ep):
221
+ xb = torch.from_numpy(xb).to(dev)
222
+ yb = torch.from_numpy(yb).to(dev)
223
+ loss = F.mse_loss(m(xb, task="rul"), yb)
224
+ opt.zero_grad()
225
+ loss.backward()
226
+ nn.utils.clip_grad_norm_(m.parameters(), 1.0)
227
+ opt.step()
228
+ sch.step()
229
+ m.eval()
230
+ with torch.no_grad():
231
+ pr = np.clip(
232
+ torch.cat(
233
+ [
234
+ m(xte_t[i : i + 256], task="rul")
235
+ for i in range(0, len(xte_t), 256)
236
+ ]
237
+ )
238
+ .cpu()
239
+ .numpy()
240
+ * cap,
241
+ 0,
242
+ cap,
243
+ )
244
+ rmse, score = cmapss_score(yte, pr)
245
+ if rmse < best:
246
+ best = rmse
247
+ torch.save(
248
+ {"cfg": cfg.__dict__, "model": m.state_dict(), "rul_cap": cap}, a.out
249
+ )
250
+ print(f"Ep {ep:02d} RMSE {rmse:.2f} score {score:.0f} best {best:.2f}")
251
+
252
+
253
+ def main():
254
+ ap = argparse.ArgumentParser()
255
+ ap.add_argument("--task", choices=["cwru", "cmapss"], required=True)
256
+ ap.add_argument("--data", default=None)
257
+ ap.add_argument("--subset", default="FD001")
258
+ ap.add_argument("--out", default=None)
259
+ ap.add_argument("--epochs", type=int, default=40)
260
+ ap.add_argument("--bs", type=int, default=64)
261
+ ap.add_argument("--lr", type=float, default=0.002)
262
+ ap.add_argument("--win", type=int, default=30)
263
+ ap.add_argument("--stride", type=int, default=1024)
264
+ ap.add_argument("--dropout", type=float, default=0.0)
265
+ ap.add_argument("--device", default="cpu")
266
+ ap.add_argument("--seed", type=int, default=0)
267
+ ap.add_argument("--smoke", action="store_true")
268
+ a = ap.parse_args()
269
+ if a.task == "cwru":
270
+ a.data = a.data or "../data/cwru"
271
+ a.out = a.out or "./pdm_cwru_fno.pt"
272
+ train_cwru(a)
273
+ else:
274
+ a.data = a.data or "../data/cmapss"
275
+ a.out = a.out or f"./pdm_cmapss_{a.subset}_fno.pt"
276
+ train_cmapss(a)
277
+
278
+
279
+ if __name__ == "__main__":
280
+ main()
verify.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ SAMPLES = {
10
+ "cmapss_FD001": torch.full((1, 30, 14), 0.5),
11
+ "cmapss_FD002": torch.full((1, 30, 14), 0.5),
12
+ "cmapss_FD003": torch.full((1, 30, 14), 0.5),
13
+ "cmapss_FD004": torch.full((1, 30, 14), 0.5),
14
+ "cwru": torch.linspace(-1, 1, 2048).reshape(1, 2048, 1),
15
+ }
16
+ VERIFICATION = {
17
+ "cmapss_FD001": {"value": 0.390998, "tol": 0.001},
18
+ "cmapss_FD002": {"value": 0.038257, "tol": 0.001},
19
+ "cmapss_FD003": {"value": 0.788864, "tol": 0.001},
20
+ "cmapss_FD004": {"value": 0.414077, "tol": 0.001},
21
+ "cwru": {"value": 0, "tol": 0.001},
22
+ }
23
+ EXPECTED_SHAPE = {
24
+ "cmapss_FD001": (1,),
25
+ "cmapss_FD002": (1,),
26
+ "cmapss_FD003": (1,),
27
+ "cmapss_FD004": (1,),
28
+ "cwru": (1, 10),
29
+ }
30
+
31
+
32
+ def main():
33
+ ap = argparse.ArgumentParser()
34
+ ap.add_argument("--variant", default="cmapss_FD001", choices=list(SAMPLES))
35
+ ap.add_argument("--weights", default=os.environ.get("FELA_PDM_WEIGHTS", "."))
36
+ args = ap.parse_args()
37
+ model = load_model(args.weights, variant=args.variant)
38
+ x = SAMPLES[args.variant]
39
+ with torch.no_grad():
40
+ out = model(x)
41
+ exp = EXPECTED_SHAPE[args.variant]
42
+ if tuple(out.shape) != exp:
43
+ print(f"Fail: output shape {tuple(out.shape)} != expected {exp}")
44
+ sys.exit(1)
45
+ print(f"Shape OK: {tuple(out.shape)}")
46
+ g = VERIFICATION[args.variant]
47
+ if g["value"] is None:
48
+ if args.variant.startswith("cmapss"):
49
+ captured = float(out.reshape(-1)[0])
50
+ else:
51
+ captured = int(out.argmax(-1).item())
52
+ print(f"Captured output: {captured}")
53
+ print(
54
+ "Verification value is a placeholder. Paste this captured value into VERIFICATION and re-run to enable the check. Shape check passed."
55
+ )
56
+ return
57
+ if args.variant.startswith("cmapss"):
58
+ got = float(out.reshape(-1)[0])
59
+ if abs(got - g["value"]) > g["tol"]:
60
+ print(f"Fail: RUL {got} vs verification {g['value']} (tol {g['tol']})")
61
+ sys.exit(1)
62
+ else:
63
+ got = int(out.argmax(-1).item())
64
+ if got != g["value"]:
65
+ print(f"Fail: class {got} vs verification {g['value']}")
66
+ sys.exit(1)
67
+ print("Verification check OK")
68
+
69
+
70
+ if __name__ == "__main__":
71
+ main()