itstheraj commited on
Commit
3476d8e
·
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,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ - energy-forecasting
12
+ - solar-power
13
+ - wind-power
14
+ - probabilistic-forecasting
15
+ - quantile-regression
16
+ library_name: transformers
17
+ pipeline_tag: time-series-forecasting
18
+ ---
19
+
20
+ # DISCLAIMER
21
+
22
+ This model is a research preview. GEFCom2014 is competition data with no stated reuse or
23
+ commercial use grant (citation only), so respect that before any commercial use. Lowdown Labs
24
+ has put together this model in the interest of advancing public science.
25
+
26
+ # FELA Power Grid: on station probabilistic solar and wind power forecasting
27
+
28
+ This model forecasts how much power a solar farm or a wind farm will produce in the
29
+ coming hours, and it gives a full range of outcomes (not just a single guess) so a grid
30
+ operator can plan for the good case and the bad case. It is small enough to fit the
31
+ envelope of a cheap microcontroller right at the station, with no cloud and no network
32
+ connection, running there through an ONNX or TFLite export.
33
+
34
+ # What goes in, what comes out
35
+
36
+ - Input: a short window of weather forecast numbers for the site (numerical weather
37
+ prediction, or NWP, covariates such as forecast irradiance for solar or forecast wind
38
+ speed and direction for wind). Solar input shape is (1, 6, 20): 6 time steps, 20
39
+ weather features. Wind input shape is (1, 12, 15): 12 time steps, 15 weather features.
40
+ - Output: 99 numbers per forecast hour, the 1 percent through 99 percent quantiles of
41
+ power output, normalized to the site's rated capacity (0 to 1). A quantile is a "what
42
+ if" level: the 10 percent quantile (P10) is a low estimate that power should exceed
43
+ 90 percent of the time, the 90 percent quantile (P90) is a high estimate. The gap
44
+ between P10 and P90 is the uncertainty band the operator plans against.
45
+ - This lets a grid or plant operator schedule reserves, bid into a market, and manage
46
+ ramps with a calibrated sense of risk, using a device that sits on the station and
47
+ keeps working when the network is down.
48
+
49
+ # Why we built it this way
50
+
51
+ The model is a dual path Fourier Neural Operator, or FNO. The main path mixes information in the
52
+ frequency domain: a fast Fourier transform, a learned filter on the frequencies, then a transform
53
+ back. Weather and power move on daily and seasonal cycles, which is cheap to capture this way.
54
+ Alongside it runs a small local mixer for the short range detail.
55
+
56
+ Everything is kept deliberately small: 136,780 parameters for solar, 311,554 for wind. Quantized to
57
+ 8 bit integers each track is under a megabyte, and a single hourly forecast takes under a
58
+ millisecond on one CPU core (measured on an x86 server CPU). That is small enough to sit on a
59
+ microcontroller at the station, running through the ONNX or TFLite export. Because it runs there
60
+ offline, the site's operational data never leaves the premises.
61
+
62
+ # Performance
63
+
64
+ Speed and footprint, measured on CPU (AMD EPYC 9555, batch size 1, median of 20 runs).
65
+
66
+ | Track | Parameters | fp32 size | int8 size | Latency, 1 core |
67
+ |---|---|---|---|---|
68
+ | Solar, input (1, 6, 20) | 136,780 | 0.74 MB | 504 KB | 0.386 ms |
69
+ | Wind, input (1, 12, 15) | 311,554 | 1.91 MB | 1472 KB | 0.375 ms |
70
+
71
+ The int8 weights are the on device deploy size. The int8 pinball loss is essentially
72
+ unchanged from fp32 (see Accuracy), so quantization is effectively lossless here.
73
+
74
+ # Accuracy
75
+
76
+ The benchmark is GEFCom2014, the standard Global Energy Forecasting Competition dataset
77
+ (Hong et al. 2016). The protocol is the final task (Task 15): train on all data before the
78
+ held out test month and forecast that month from weather inputs only, with no test period
79
+ power used (so there is no leakage).
80
+
81
+ Solar is 3 zones (test month June 2014), wind is 10
82
+ zones (test month December 2013). The metric is pinball loss averaged over the 1 to 99
83
+ percent quantiles, with power normalized to site capacity, exactly as in the competition.
84
+ Lower pinball loss is better. "Skill" is the percent reduction in pinball loss against a
85
+ named reference forecast.
86
+
87
+ | Benchmark | Metric | This model | Baseline (named) | Source |
88
+ |---|---|---|---|---|
89
+ | GEFCom2014 solar | pinball (norm.) | 0.01308 (int8 0.01328) | competition benchmark 0.0285 | measured (ours) |
90
+ | GEFCom2014 solar | skill vs competition benchmark | +54.1 percent | competition benchmark | measured (ours) |
91
+ | GEFCom2014 solar | skill vs diurnal persistence | +32.1 percent | diurnal persistence | measured (ours) |
92
+ | GEFCom2014 solar | pinball (norm.) | 0.01308 | our LightGBM quantile baseline 0.01232 | measured (ours) |
93
+ | GEFCom2014 solar | pinball (norm.) | 0.01308 | published LSTM/quantile NN 0.0143 | published |
94
+ | GEFCom2014 wind | pinball (norm.) | 0.04690 (int8 0.04692) | competition benchmark 0.0792 | measured (ours) |
95
+ | GEFCom2014 wind | skill vs competition benchmark | +40.8 percent | competition benchmark | measured (ours) |
96
+ | GEFCom2014 wind | skill vs diurnal persistence | +47.5 percent | diurnal persistence | measured (ours) |
97
+ | GEFCom2014 wind | pinball (norm.) | 0.04690 | our LightGBM quantile baseline 0.04547 | measured (ours) |
98
+ | GEFCom2014 wind | pinball (norm.) | 0.04690 | published GAN / normalizing flow / VAE / DDPM | published |
99
+
100
+ The model wins the official GEFCom2014 benchmark on both tracks, and it beats
101
+ the competition benchmark by wide margins (skill +54.1 percent on solar, +40.8 percent on wind).
102
+ What it is not is a new raw pinball record.
103
+
104
+ On the identical pipeline it ties our own LightGBM
105
+ gradient boosted baseline (solar 0.01308 vs 0.01232, wind 0.04690 vs 0.04547). It beats a
106
+ published LSTM/quantile NN on solar and a published GAN on wind, sits level with a published
107
+ normalizing flow, and lands a few percent behind the best published diffusion model (VAE/DDPM)
108
+ on wind.
109
+
110
+ So - the accuracy is competitive but with given resources, not chart topping. The real edge is where it delivers
111
+ that accuracy: 136,780 and 311,554 parameters, under a megabyte in int8, sub millisecond on a
112
+ CPU, running on station with the network gapped - which we feel is a great domain adaptation for our methodologies.
113
+
114
+ # How to run it
115
+
116
+ See `quickstart/` for a runnable example. The model loads in a few lines with the bundled
117
+ `modeling.py` plus `config.json`, from the safe `safetensors` weight file (no pickle):
118
+
119
+ ```python
120
+ from huggingface_hub import hf_hub_download
121
+ import modeling # bundled in this repo
122
+
123
+ path = hf_hub_download("lowdown-labs/fela-power-grid", "solar.safetensors")
124
+ model = modeling.load_model(path, track="solar")
125
+
126
+ # Preprocess a raw NWP window (shape (6, 20) for solar), then forecast.
127
+ x = modeling.preprocess_nwp(raw_window, track="solar") # validates shape, standardizes
128
+ import torch
129
+ with torch.no_grad():
130
+ quantiles = model(x) # (1, 99): the P1..P99 power quantiles for the center forecast hour
131
+
132
+ # A P10 to P90 band for the center forecast hour:
133
+ p10 = quantiles[0, 9].item()
134
+ p90 = quantiles[0, 89].item()
135
+ print("Center hour P10..P90 (fraction of capacity):", p10, p90)
136
+ ```
137
+
138
+ The `modeling.preprocess_nwp` helper standardizes the weather window and validates its
139
+ shape (it fails clearly on the wrong shape or units). For an interactive playground, see
140
+ the Hugging Face Space linked in this repo.
141
+
142
+ ## Formats
143
+
144
+ This model is CPU native: no GPU is required to run it, in any format. The fp32 and int8
145
+ formats run on a plain CPU.
146
+
147
+ - fp32: reference and CPU.
148
+ - int8: on device deployment format (AVX512-VNNI on x86, NEON dot product on ARM). The
149
+ int8 pinball loss is essentially unchanged (solar 0.01328 vs 0.01308, wind 0.04692 vs
150
+ 0.04690), so on device quantization is effectively lossless here.
151
+ - bf16: an optional format for server or GPU inference, not required and not the on device
152
+ format. Most commodity ARM and microcontroller CPUs lack native bf16, so use fp32 or int8
153
+ there. You never need a GPU; bf16 is only a convenience when one happens to be present.
154
+
155
+ ## Serving
156
+
157
+ For serving at scale, use the separate CPU native FELA server (https://github.com/Lowdown-Labs/fela_server). It
158
+ runs this model on CPU, with no GPU required. The quickstart in this repo is the minimal
159
+ single process path; the FELA server is the production serving path.
160
+
161
+ # Training data
162
+
163
+ - GEFCom2014 (Global Energy Forecasting Competition 2014), public competition data
164
+ released with the competition. Used for both training and the held out evaluation, under
165
+ the GEFCom2014 Task 15 protocol described above. Solar and wind tracks. Citation: Hong,
166
+ Pinson, Fan, Zareipour, Troccoli, Hyndman (2016), "Probabilistic energy forecasting:
167
+ Global Energy Forecasting Competition 2014 and beyond," International Journal of
168
+ Forecasting 32(3). The dataset is the public competition release; check the competition
169
+ terms for the exact redistribution conditions before rehosting it.
170
+
171
+ No proprietary or customer data was used. The model takes only numerical weather
172
+ prediction covariates as input.
173
+
174
+ ## Training data, splits and licensing
175
+
176
+ The training and held out evaluation splits are defined in `train.py` in this repo. A `--smoke`
177
+ flag rebuilds the split, asserts the audited held out window count per track, and exits before
178
+ training.
179
+
180
+ - Dataset: GEFCom2014 (Global Energy Forecasting Competition 2014), solar and wind tracks.
181
+ Version: the Task 15 (final task) public release, as distributed with the paper. The solar
182
+ track has 3 zones with 12 NWP predictors; the wind track has 10 zones with 4 NWP predictors.
183
+ - Source: the data was released as the appendix of the GEFCom2014 paper and mirrored by the
184
+ competition General Chair at http://blog.drhongtao.com/2017/03/gefcom2014-load-forecasting-data.html
185
+ (and via ScienceDirect, DOI 10.1016/j.ijforecast.2016.02.001). Citation: Hong, Pinson, Fan,
186
+ Zareipour, Troccoli, Hyndman (2016), International Journal of Forecasting 32(3), 896-913.
187
+ - Split: GEFCom2014 Task 15 protocol, a fixed calendar held out month, not a random split.
188
+ Solar test month is June 2014 (2014-06-01 01:00 to 2014-07-01 00:00); wind test month is
189
+ December 2013. Training uses all prior data; the test month is forecast from NWP only (no
190
+ test period power, so no autoregressive leakage). The held out and train membership is the
191
+ `is_test` flag defined in `train.py`, and `train.py --smoke` asserts the audited held out
192
+ counts: solar 2154 windows over 3 zones, wind 7390 windows over 10 zones. A 6 percent tail
193
+ of the training rows is held out as a validation set for early stopping (deterministic tail
194
+ slice).
195
+ - License: NO license is stated for GEFCom2014. The data is competition data delivered as an
196
+ appendix to a copyrighted (all rights reserved) Elsevier / International Journal of
197
+ Forecasting article; the author distribution page states only a citation requirement, with no
198
+ reuse or commercial use grant. Underlying source data (e.g. ISO New England for the extended
199
+ load track) may carry its own upstream terms, and the distributor warns against combining the
200
+ datasets.
201
+ - Commercial verdict: UNCLEAR and UNSTATED. Competition data, no license grant, the highest risk
202
+ of the family. Citation alone is not a commercial use grant. For any commercial or
203
+ redistribution use, obtain written permission from the organizers (Tao Hong / International
204
+ Institute of Forecasters) and verify the upstream source terms first.
205
+
206
+ # Citations and licenses
207
+
208
+ This section consolidates the formal references and the direct links to the real license
209
+ text for every dataset and method used, verified from source.
210
+
211
+ ## Datasets
212
+
213
+ - **GEFCom2014 (Global Energy Forecasting Competition 2014)**: solar and wind tracks, the
214
+ Task 15 protocol used for both training and the held out evaluation.
215
+ - Reference: Hong, T., Pinson, P., Fan, S., Zareipour, H., Troccoli, A., & Hyndman, R. J.
216
+ (2016). Probabilistic energy forecasting: Global Energy Forecasting Competition 2014 and
217
+ beyond. *International Journal of Forecasting*, 32(3), 896-913.
218
+ DOI: [10.1016/j.ijforecast.2016.02.001](https://doi.org/10.1016/j.ijforecast.2016.02.001)
219
+ - Data and terms: released as the appendix of the paper and mirrored by the competition General
220
+ Chair at
221
+ [blog.drhongtao.com/2017/03/gefcom2014-load-forecasting-data.html](http://blog.drhongtao.com/2017/03/gefcom2014-load-forecasting-data.html).
222
+ The paper itself is (c) Elsevier / International Journal of Forecasting (all rights reserved):
223
+ [ScienceDirect article page](https://www.sciencedirect.com/science/article/pii/S0169207016000133).
224
+ - **License: NO reuse or commercial use grant is stated, COMPETITION TERMS.** The distribution
225
+ page states only a citation requirement; the data is delivered as an appendix to a copyrighted
226
+ Elsevier article, and underlying source data (e.g. ISO New England for the extended load track)
227
+ may carry its own upstream terms. Citation is not a commercial use grant. For any commercial or
228
+ redistribution use, obtain written permission from the organizers (Tao Hong / International
229
+ Institute of Forecasters) and verify the upstream source terms first. See the fuller caveat under
230
+ "Training data, splits and licensing" above.
231
+
232
+ ## Methods and code
233
+
234
+ - **Fourier Neural Operator (FNO)**: the sequence mixer at the core of both tracks.
235
+ Li, Z., Kovachki, N., Azizzadenesheli, K., Liu, B., Bhattacharya, K., Stuart, A., & Anandkumar, A.
236
+ (2021). Fourier Neural Operator for Parametric Partial Differential Equations. *ICLR*.
237
+ [arXiv:2010.08895](https://arxiv.org/abs/2010.08895)
238
+ - **PyTorch**: training and inference framework. Paszke, A., et al. (2019). PyTorch: An
239
+ Imperative Style, High-Performance Deep Learning Library. *NeurIPS*.
240
+ [arXiv:1912.01703](https://arxiv.org/abs/1912.01703)
241
+ - **LightGBM**: the reference gradient boosted quantile baseline. Ke, G., et al. (2017).
242
+ LightGBM: A Highly Efficient Gradient Boosting Decision Tree. *NeurIPS*.
243
+ [proceedings](https://papers.nips.cc/paper_files/paper/2017/hash/6449f44a102fde848669bdd9eb6b76fa-Abstract.html)
244
+ - **NumPy / SciPy**: used in the data preparation and evaluation pipeline; standard scientific
245
+ Python stack. Harris et al. (2020), [Nature 585, 357-362](https://doi.org/10.1038/s41586-020-2649-2);
246
+ Virtanen et al. (2020), [Nature Methods 17, 261-272](https://doi.org/10.1038/s41592-019-0686-2).
247
+ - **ONNX Runtime / TFLite**: the on device export and runtime path.
248
+ [onnxruntime.ai](https://onnxruntime.ai/),
249
+ [ai.google.dev/edge/litert](https://ai.google.dev/edge/litert).
250
+
251
+ This model does not use Gated Linear Attention, Gated DeltaNet, or Landmark Attention: both
252
+ tracks are pure FNO (see `modeling.py` and `train.py`).
253
+
254
+ # Intended use, limitations, and safety
255
+
256
+ What it is for:
257
+
258
+ - Short horizon probabilistic power forecasting at a solar or wind site, driven by a
259
+ numerical weather prediction feed, on station or on device.
260
+
261
+ What it is not for:
262
+
263
+ - It is not a single point guarantee of output and not a replacement for a grid operator's
264
+ judgment. The quantile band is a planning aid, not a control signal.
265
+ - It is not validated for direct, unsupervised use in a safety critical or
266
+ protection critical control loop. Do not wire its output into automated dispatch,
267
+ protection, or curtailment that affects grid stability without independent validation and
268
+ a human or rule based check in the loop.
269
+
270
+ Evaluated conditions and known limits:
271
+
272
+ - Evaluated only on GEFCom2014 (3 solar zones, 10 wind zones) under the Task 15 protocol.
273
+ Performance on other sites, climates, turbine types, or NWP feeds is not characterized
274
+ here and should be validated before operational use.
275
+ - The model is a tie with a gradient boosted (LightGBM) baseline on raw accuracy and is a
276
+ few percent behind the best published diffusion/VAE on wind. If raw pinball loss is the
277
+ only thing that matters and device size does not, those baselines are reasonable
278
+ alternatives. The reason to choose this model is the on station, sub megabyte,
279
+ sub millisecond, network gapped deployment.
280
+ - A separate test on SDWPF (Baidu KDD Cup 2022), a harder modern wind farm benchmark, did
281
+ not transfer well and is not claimed here. The genuine climate win is GEFCom2014.
282
+ - The quantile outputs are calibrated against the GEFCom2014 evaluation only. Verify
283
+ calibration on your own data before relying on the P10 to P90 band for reserve sizing.
284
+
285
+ Privacy:
286
+
287
+ - The model runs on station and offline. When deployed on device, the site's operational
288
+ and weather data does not leave the device, so there is no cloud round trip and no data
289
+ shared with Lowdown Labs or any third party.
290
+
291
+ # How to cite
292
+
293
+ Model and technical note:
294
+
295
+ ```
296
+ @misc{lowdownlabs_grid_renewable,
297
+ title = {FELA Grid Renewable: on station probabilistic solar and wind power forecasting},
298
+ author = {Lowdown Labs},
299
+ year = {2026},
300
+ note = {Model card}
301
+ }
302
+ ```
303
+
304
+ You must also cite the benchmark dataset and the core libraries:
305
+
306
+ - Hong, T., Pinson, P., Fan, S., Zareipour, H., Troccoli, A., Hyndman, R. J. (2016).
307
+ Probabilistic energy forecasting: Global Energy Forecasting Competition 2014 and beyond.
308
+ International Journal of Forecasting, 32(3), 896 to 913.
309
+ - Li, Z., Kovachki, N., Azizzadenesheli, K., Liu, B., Bhattacharya, K., Stuart, A.,
310
+ Anandkumar, A. (2021). Fourier Neural Operator for Parametric Partial Differential
311
+ Equations. International Conference on Learning Representations (ICLR).
312
+ - Paszke, A. et al. (2019). PyTorch: An Imperative Style, High-Performance Deep Learning
313
+ Library. NeurIPS.
314
+
315
+ # Acknowledgements and references
316
+
317
+ - GEFCom2014: Hong et al. (2016), International Journal of Forecasting 32(3), 896 to 913.
318
+ - Fourier Neural Operator: Li et al. (2021), ICLR. The architecture is built on the FNO.
319
+ - LightGBM (our reference quantile baseline): Ke, G. et al. (2017). LightGBM: A Highly
320
+ Efficient Gradient Boosting Decision Tree. NeurIPS.
321
+ - PyTorch: Paszke et al. (2019), NeurIPS.
322
+
323
+ # Model family
324
+
325
+ This is part of the FELA family from Lowdown Labs: one FNO architecture across many
326
+ modalities, all CPU native and subquadratic. This repo is published as
327
+ `lowdown-labs/fela-power-grid`. The sibling repos are:
328
+
329
+ - `lowdown-labs/fela-genomics`: DNA sequence classification.
330
+ - `lowdown-labs/fela-pdm`: rotating machinery and turbofan health.
331
+ - `lowdown-labs/fela-power-grid` (this repo): probabilistic solar and wind power forecasting.
332
+ - `lowdown-labs/fela-video`: video moment retrieval and temporal grounding.
333
+ - `lowdown-labs/fela-streaming-asr`: streaming CPU speech recognition.
334
+
335
+ These are grouped under the FELA Collection on Hugging Face. The models are independently
336
+ trained per modality and do not share weights, so none carries a `base_model` link.
337
+
338
+ # License
339
+
340
+ Released under the Lowdown Labs Lovely License 1.0 (CC BY-NC 4.0 plus Hippocratic License 3.0). See LICENSE. For most LL models, a commercial license may be available; contact Lowdown Labs.
config.json ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "fela_grid_renewable",
3
+ "description": "Dual path FNO probabilistic power forecaster for solar and wind, trained on GEFCom2014. Outputs 99 quantiles per hour from weather covariates.",
4
+ "architecture": "fno_dual_path",
5
+ "framework": "pytorch",
6
+ "architectures": [
7
+ "FelaGridModel"
8
+ ],
9
+ "auto_map": {
10
+ "AutoConfig": "configuration_grid.FelaGridConfig",
11
+ "AutoModel": "modeling_grid.FelaGridModel"
12
+ },
13
+ "default_track": "solar",
14
+ "tracks": {
15
+ "solar": {
16
+ "params": 136780,
17
+ "input_steps": 6,
18
+ "input_features": 20,
19
+ "input_shape": [1, 6, 20],
20
+ "weights_safetensors": "solar.safetensors",
21
+ "dims": {"Fin": 20, "L": 6, "D": 64, "modes": 3, "nblk": 4, "nq": 99, "arch": "dual"}
22
+ },
23
+ "wind": {
24
+ "params": 311554,
25
+ "input_steps": 12,
26
+ "input_features": 15,
27
+ "input_shape": [1, 12, 15],
28
+ "weights_safetensors": "wind.safetensors",
29
+ "dims": {"Fin": 15, "L": 12, "D": 96, "modes": 6, "nblk": 3, "nq": 99, "arch": "dual"}
30
+ }
31
+ },
32
+ "num_quantiles": 99,
33
+ "quantile_levels_percent": "1..99",
34
+ "output_shape": "[batch, 99]",
35
+ "output_units": "power as fraction of site rated capacity (0 to 1), 99 monotone quantiles for the center forecast hour",
36
+ "preprocessing": "standardize the NWP window per feature; RevIN runs inside the model. See modeling.preprocess_nwp",
37
+ "license": "lowdown-labs-lovely-license-1.0"
38
+ }
configuration_grid.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import PretrainedConfig
2
+
3
+
4
+ class FelaGridConfig(PretrainedConfig):
5
+ model_type = "fela_grid_renewable"
6
+
7
+ def __init__(
8
+ self,
9
+ track="solar",
10
+ Fin=20,
11
+ L=6,
12
+ D=64,
13
+ modes=3,
14
+ nblk=4,
15
+ nq=99,
16
+ arch="dual",
17
+ tracks=None,
18
+ **kwargs,
19
+ ):
20
+ if isinstance(tracks, dict) and track in tracks:
21
+ dims = tracks[track].get("dims", {})
22
+ Fin = dims.get("Fin", Fin)
23
+ L = dims.get("L", L)
24
+ D = dims.get("D", D)
25
+ modes = dims.get("modes", modes)
26
+ nblk = dims.get("nblk", nblk)
27
+ nq = dims.get("nq", nq)
28
+ arch = dims.get("arch", arch)
29
+ self.track = track
30
+ self.Fin = Fin
31
+ self.L = L
32
+ self.D = D
33
+ self.modes = modes
34
+ self.nblk = nblk
35
+ self.nq = nq
36
+ self.arch = arch
37
+ if tracks is not None:
38
+ self.tracks = tracks
39
+ super().__init__(**kwargs)
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dd972ee0aae73432de6f35325dd1458555e99ecb12e9c2edf76c793bc94b14b3
3
+ size 747808
modeling.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.nn.functional as F
6
+
7
+ CONFIG = None
8
+
9
+
10
+ def _config():
11
+ global CONFIG
12
+ if CONFIG is None:
13
+ here = os.path.dirname(os.path.abspath(__file__))
14
+ with open(os.path.join(here, "config.json")) as f:
15
+ CONFIG = json.load(f)
16
+ return CONFIG
17
+
18
+
19
+ class RevIN(nn.Module):
20
+ def __init__(self, C):
21
+ super().__init__()
22
+ self.g = nn.Parameter(torch.ones(C))
23
+ self.b = nn.Parameter(torch.zeros(C))
24
+
25
+ def norm(self, x):
26
+ self.m = x.mean(1, keepdim=True)
27
+ self.s = x.std(1, keepdim=True) + 1e-05
28
+ return (x - self.m) / self.s * self.g + self.b
29
+
30
+
31
+ class FNO1D(nn.Module):
32
+ def __init__(self, D, modes):
33
+ super().__init__()
34
+ self.modes = modes
35
+ s = 1 / (D * D)
36
+ self.w = nn.Parameter(s * torch.rand(modes, D, D, dtype=torch.cfloat))
37
+
38
+ def forward(self, x):
39
+ P = x.shape[1]
40
+ xf = torch.fft.rfft(x, dim=1)
41
+ m = min(self.modes, xf.shape[1])
42
+ o = torch.zeros_like(xf)
43
+ o[:, :m] = torch.einsum("bpd,pde->bpe", xf[:, :m], self.w[:m])
44
+ return torch.fft.irfft(o, n=P, dim=1)
45
+
46
+
47
+ class Block(nn.Module):
48
+ def __init__(self, D, modes, ff=2, drop=0.0):
49
+ super().__init__()
50
+ self.n1 = nn.LayerNorm(D)
51
+ self.fno = FNO1D(D, modes)
52
+ self.d1 = nn.Dropout(drop)
53
+ self.n2 = nn.LayerNorm(D)
54
+ self.ff = nn.Sequential(
55
+ nn.Linear(D, D * ff), nn.GELU(), nn.Dropout(drop), nn.Linear(D * ff, D)
56
+ )
57
+
58
+ def forward(self, x):
59
+ x = x + self.d1(self.fno(self.n1(x)))
60
+ return x + self.ff(self.n2(x))
61
+
62
+
63
+ class FELA_Grid(nn.Module):
64
+ def __init__(self, Fin, L, D=96, modes=6, nblk=3, nq=99, arch="dual"):
65
+ super().__init__()
66
+ self.L = L
67
+ self.arch = arch
68
+ self.center = L // 2
69
+ self.nq = nq
70
+ self.revin = RevIN(Fin)
71
+ self.embed = nn.Linear(Fin, D)
72
+ self.pos = nn.Parameter(0.02 * torch.randn(1, L, D))
73
+ self.blocks = nn.ModuleList([Block(D, modes) for _ in range(nblk)])
74
+ self.norm = nn.LayerNorm(D)
75
+ if arch == "dual":
76
+ self.direct = nn.Sequential(
77
+ nn.Linear(Fin, D), nn.GELU(), nn.Linear(D, D), nn.GELU()
78
+ )
79
+ fuse_in = 2 * D
80
+ else:
81
+ fuse_in = D
82
+ self.med = nn.Linear(fuse_in, 1)
83
+ self.spread = nn.Linear(fuse_in, nq)
84
+ self.register_buffer("qidx", torch.arange(nq))
85
+
86
+ def forward(self, x):
87
+ xc = x[:, self.center]
88
+ xn = self.revin.norm(x)
89
+ h = self.embed(xn) + self.pos
90
+ for b in self.blocks:
91
+ h = b(h)
92
+ ctx = self.norm(h)[:, self.center]
93
+ if self.arch == "dual":
94
+ z = torch.cat([ctx, self.direct(xc)], dim=1)
95
+ else:
96
+ z = ctx
97
+ med = torch.sigmoid(self.med(z))
98
+ w = F.softplus(self.spread(z))
99
+ half = self.nq // 2
100
+ below = -torch.flip(torch.cumsum(torch.flip(w[:, :half], [1]), 1), [1])
101
+ above = torch.cumsum(w[:, half + 1 :], 1)
102
+ offs = (
103
+ torch.cat([below, torch.zeros_like(w[:, half : half + 1]), above], dim=1)
104
+ * 0.05
105
+ )
106
+ return torch.clamp(med + offs, 0, 1)
107
+
108
+
109
+ def expected_shape(track):
110
+ t = _config()["tracks"][track]
111
+ return (t["input_steps"], t["input_features"])
112
+
113
+
114
+ def validate_input(x, track):
115
+ steps, feats = expected_shape(track)
116
+ if not isinstance(x, torch.Tensor):
117
+ raise TypeError(f"Expected a torch.Tensor, got {type(x)}")
118
+ if x.dim() != 3:
119
+ raise ValueError(
120
+ f"{track}: expected a 3-D tensor (batch, steps, features), got shape {tuple(x.shape)}"
121
+ )
122
+ if x.shape[1] != steps or x.shape[2] != feats:
123
+ raise ValueError(
124
+ f"{track}: expected window (batch, {steps}, {feats}), got {tuple(x.shape)}. Solar windows are (.,6,20); wind windows are (.,12,15)."
125
+ )
126
+ return x
127
+
128
+
129
+ def preprocess_nwp(raw_window, track, mean=None, std=None):
130
+ x = torch.as_tensor(raw_window, dtype=torch.float32)
131
+ if x.dim() == 2:
132
+ x = x.unsqueeze(0)
133
+ if mean is not None and std is not None:
134
+ mean = torch.as_tensor(mean, dtype=torch.float32)
135
+ std = torch.as_tensor(std, dtype=torch.float32)
136
+ x = (x - mean) / torch.clamp(std, min=1e-06)
137
+ else:
138
+ m = x.mean(dim=(0, 1), keepdim=True)
139
+ s = x.std(dim=(0, 1), keepdim=True)
140
+ x = (x - m) / torch.clamp(s, min=1e-06)
141
+ return validate_input(x, track)
142
+
143
+
144
+ def _build_from_state(state, track):
145
+ dims = _config()["tracks"][track]["dims"]
146
+ model = FELA_Grid(
147
+ dims["Fin"],
148
+ dims["L"],
149
+ D=dims["D"],
150
+ modes=dims["modes"],
151
+ nblk=dims["nblk"],
152
+ nq=dims["nq"],
153
+ arch=dims.get("arch", "dual"),
154
+ )
155
+ ref = model.state_dict()
156
+ fixed = {}
157
+ for k, v in state.items():
158
+ t = ref.get(k)
159
+ if t is not None and t.is_complex() and (not v.is_complex()):
160
+ v = torch.view_as_complex(v.contiguous())
161
+ fixed[k] = v
162
+ fixed["qidx"] = model.qidx.clone()
163
+ model.load_state_dict(fixed, strict=True)
164
+ return model
165
+
166
+
167
+ def load_model(path_or_repo, track="solar", filename=None):
168
+ path = path_or_repo
169
+ fname = filename or _config()["tracks"][track]["weights_safetensors"]
170
+ if os.path.isdir(path):
171
+ path = os.path.join(path, fname)
172
+ elif not os.path.exists(path):
173
+ from huggingface_hub import hf_hub_download
174
+
175
+ path = hf_hub_download(path_or_repo, fname)
176
+ from safetensors.torch import load_file
177
+
178
+ state = load_file(path)
179
+ model = _build_from_state(state, track)
180
+ model.eval()
181
+ return model
182
+
183
+
184
+ def from_pretrained(repo_id, track="solar"):
185
+ return load_model(repo_id, track=track)
modeling_grid.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import types
4
+
5
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
6
+ import torch
7
+ import torch.nn as nn
8
+ from transformers import PreTrainedModel
9
+ from transformers.modeling_outputs import CausalLMOutput
10
+
11
+ from .configuration_grid import FelaGridConfig
12
+ from .modeling import FELA_Grid, FNO1D
13
+
14
+
15
+ def _fno1d_forward(self, x):
16
+ w = torch.view_as_complex(self.w)
17
+ P = x.shape[1]
18
+ xf = torch.fft.rfft(x, dim=1)
19
+ mm = min(self.modes, xf.shape[1])
20
+ o = torch.zeros_like(xf)
21
+ o[:, :mm] = torch.einsum("bpd,pde->bpe", xf[:, :mm], w[:mm])
22
+ return torch.fft.irfft(o, n=P, dim=1)
23
+
24
+
25
+ def _realify(model):
26
+ for m in model.modules():
27
+ if isinstance(m, FNO1D):
28
+ m.w = nn.Parameter(torch.view_as_real(m.w.detach()).contiguous())
29
+ m.forward = types.MethodType(_fno1d_forward, m)
30
+
31
+
32
+ class FelaGridModel(PreTrainedModel):
33
+ config_class = FelaGridConfig
34
+ base_model_prefix = "model"
35
+ main_input_name = "x"
36
+
37
+ def __init__(self, config):
38
+ super().__init__(config)
39
+ self.model = FELA_Grid(
40
+ config.Fin,
41
+ config.L,
42
+ D=config.D,
43
+ modes=config.modes,
44
+ nblk=config.nblk,
45
+ nq=config.nq,
46
+ arch=config.arch,
47
+ )
48
+ self.model._non_persistent_buffers_set.add("qidx")
49
+ _realify(self.model)
50
+ self.post_init()
51
+
52
+ def forward(self, x=None, input_values=None, **kwargs):
53
+ if x is None:
54
+ x = input_values
55
+ out = self.model(x)
56
+ return CausalLMOutput(logits=out)
quickstart/README.md ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Quickstart: FELA Grid Renewable
2
+
3
+ A minimal example that loads the forecaster, runs one weather input window, and prints the
4
+ P10 to P90 power band for the first forecast hour. It runs on CPU; no GPU is required.
5
+
6
+ ## Install
7
+
8
+ Pinned versions, runs from a clean virtual environment:
9
+
10
+ ```
11
+ pip install -r requirements.txt
12
+ ```
13
+
14
+ ## Get the weights
15
+
16
+ The safetensors weights ship in the model repo (`solar.safetensors`, `wind.safetensors`).
17
+ `--weights` defaults to the track's safetensors file, or point it at a local path.
18
+
19
+ ## Run
20
+
21
+ Solar (default):
22
+
23
+ ```
24
+ python run.py --track solar --weights /path/to/solar.safetensors
25
+ ```
26
+
27
+ Wind:
28
+
29
+ ```
30
+ python run.py --track wind --weights /path/to/wind.safetensors
31
+ ```
32
+
33
+ The script loads with the bundled `modeling.load_model` (a few line load) and preprocesses
34
+ the window with `modeling.preprocess_nwp`, which standardizes it and validates the shape
35
+ (it fails clearly on the wrong shape).
36
+
37
+ ## What you should see
38
+
39
+ The script prints the output shape `(1, 99)` and, for the center forecast hour,
40
+ the P10 (low), P50 (median), and P90 (high) power levels as a fraction of site capacity,
41
+ plus the width of the P10 to P90 uncertainty band. Input shapes are solar `(1, 6, 20)`,
42
+ wind `(1, 12, 15)`.
43
+
44
+ The example uses a random input window so it runs without a data download. Replace it with
45
+ a real NWP window (the same shape) to get a real forecast.
quickstart/requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ torch==2.3.1
2
+ numpy==1.26.4
3
+ safetensors==0.4.3
4
+ huggingface_hub==0.23.4
quickstart/run.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import os
3
+ import sys
4
+ import torch
5
+
6
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
7
+ import modeling
8
+
9
+
10
+ def main():
11
+ ap = argparse.ArgumentParser()
12
+ ap.add_argument("--track", choices=["solar", "wind"], default="solar")
13
+ ap.add_argument(
14
+ "--weights",
15
+ default=None,
16
+ help="path to the track's safetensors (defaults to <track>.safetensors)",
17
+ )
18
+ args = ap.parse_args()
19
+ weights = args.weights or f"{args.track}.safetensors"
20
+ steps, feats = modeling.expected_shape(args.track)
21
+ raw_window = torch.randn(steps, feats)
22
+ nwp = modeling.preprocess_nwp(raw_window, track=args.track)
23
+ model = modeling.load_model(weights, track=args.track)
24
+ with torch.no_grad():
25
+ quantiles = model(nwp)
26
+ p10 = quantiles[0, 9].item()
27
+ p50 = quantiles[0, 49].item()
28
+ p90 = quantiles[0, 89].item()
29
+ print(f"Track: {args.track}")
30
+ print(
31
+ f"Output shape: {tuple(quantiles.shape)} (batch, 99 quantiles for the center hour)"
32
+ )
33
+ print("Center forecast hour, power as fraction of site capacity:")
34
+ print(f" P10 (low): {p10:.4f}")
35
+ print(f" P50 (median): {p50:.4f}")
36
+ print(f" P90 (high): {p90:.4f}")
37
+ print(f" P10..P90 uncertainty band width: {p90 - p10:.4f}")
38
+
39
+
40
+ if __name__ == "__main__":
41
+ main()
solar.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dd972ee0aae73432de6f35325dd1458555e99ecb12e9c2edf76c793bc94b14b3
3
+ size 747808
space/README.md ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: FELA Grid Renewable
3
+ emoji: power
4
+ colorFrom: green
5
+ colorTo: gray
6
+ sdk: gradio
7
+ sdk_version: 4.44.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: other
11
+ ---
12
+
13
+ # FELA Grid Renewable playground
14
+
15
+ Pick solar or wind, pick a sample weather window, and see the model's probabilistic power
16
+ forecast: the P10 low estimate, the P50 median, and the P90 high estimate, all as a
17
+ fraction of the site's rated capacity. The gap between P10 and P90 is the uncertainty band
18
+ a grid operator plans against.
19
+
20
+ ## Play data
21
+
22
+ The sample weather windows are a small GEFCom2014 style set of numerical weather
23
+ prediction (NWP) inputs. GEFCom2014 is the public Global Energy Forecasting Competition
24
+ 2014 dataset (Hong, Pinson, Fan, Zareipour, Troccoli, Hyndman, 2016, International Journal
25
+ of Forecasting 32(3)). It is the public competition release; check the competition terms
26
+ before rehosting it. The samples are kept small so the Space loads fast.
27
+
28
+ If the bundled sample (`solar_demo.json`, `wind_demo.json`) is not present, the app falls
29
+ back to synthetic NWP windows of the correct shape so the Space still runs. The synthetic
30
+ fallback shows the input and output shapes working, not a real site forecast.
31
+
32
+ Input shapes are solar `(1, 6, 20)`, wind `(1, 12, 15)`.
33
+
34
+ ## Weights
35
+
36
+ The safetensors weight files (`solar.safetensors`, `wind.safetensors`) ship with the model
37
+ and load through the bundled `modeling.load_model`. If they are absent the app shows a notice.
38
+
39
+ ## License
40
+
41
+ Released under the Lowdown Labs Lovely License 1.0. See the model repo for the
42
+ full license text.
space/app.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import sys
4
+ import gradio as gr
5
+ import numpy as np
6
+ import torch
7
+
8
+ HERE = os.path.dirname(os.path.abspath(__file__))
9
+ sys.path.insert(0, os.path.join(HERE, ".."))
10
+ SHAPES = {"solar": (6, 20), "wind": (12, 15)}
11
+ WEIGHTS_SAFE = {
12
+ "solar": os.path.join(HERE, "solar.safetensors"),
13
+ "wind": os.path.join(HERE, "wind.safetensors"),
14
+ }
15
+ WEIGHTS_PT = {
16
+ "solar": os.path.join(HERE, "solar.pt"),
17
+ "wind": os.path.join(HERE, "wind.pt"),
18
+ }
19
+ SAMPLES = {
20
+ "solar": os.path.join(HERE, "solar_demo.json"),
21
+ "wind": os.path.join(HERE, "wind_demo.json"),
22
+ }
23
+ _models = {}
24
+
25
+
26
+ def load_model(track):
27
+ if track not in _models:
28
+ if os.path.exists(WEIGHTS_SAFE[track]):
29
+ import modeling
30
+
31
+ m = modeling.load_model(WEIGHTS_SAFE[track], track=track)
32
+ m.eval()
33
+ _models[track] = m
34
+ elif os.path.exists(WEIGHTS_PT[track]):
35
+ m = torch.load(WEIGHTS_PT[track], map_location="cpu", weights_only=False)
36
+ m.eval()
37
+ _models[track] = m
38
+ else:
39
+ _models[track] = None
40
+ return _models[track]
41
+
42
+
43
+ def load_sample_windows(track):
44
+ path = SAMPLES[track]
45
+ steps, feats = SHAPES[track]
46
+ if os.path.exists(path):
47
+ with open(path) as f:
48
+ data = json.load(f)
49
+ windows = data.get("windows", data) if isinstance(data, dict) else data
50
+ return [np.asarray(w, dtype=np.float32).reshape(steps, feats) for w in windows]
51
+ rng = np.random.default_rng(0)
52
+ return [rng.standard_normal((steps, feats)).astype(np.float32) for _ in range(8)]
53
+
54
+
55
+ def forecast(track, sample_idx):
56
+ steps, feats = SHAPES[track]
57
+ windows = load_sample_windows(track)
58
+ sample_idx = int(sample_idx) % len(windows)
59
+ nwp = torch.from_numpy(windows[sample_idx]).unsqueeze(0)
60
+ model = load_model(track)
61
+ if model is None:
62
+ return "Weights are not found in this Space. Ensure solar.safetensors / wind.safetensors are present next to app.py."
63
+ with torch.no_grad():
64
+ q = model(nwp)
65
+ q = q[0].cpu().numpy()
66
+ p10, p50, p90 = (q[9], q[49], q[89])
67
+ lines = [
68
+ f"Track: {track}",
69
+ f"Sample window {sample_idx} of {len(windows)}",
70
+ "First forecast hour (power as a fraction of site capacity, 0 to 1):",
71
+ f" P10 (low estimate): {p10:.4f}",
72
+ f" P50 (median forecast): {p50:.4f}",
73
+ f" P90 (high estimate): {p90:.4f}",
74
+ f" P10 to P90 band width: {p90 - p10:.4f}",
75
+ "",
76
+ "The P10 to P90 band is the range the actual output should fall in most of the",
77
+ "time. A wide band means the weather input is uncertain for this hour.",
78
+ ]
79
+ return "\n".join(lines)
80
+
81
+
82
+ with gr.Blocks(title="FELA Grid Renewable") as demo:
83
+ gr.Markdown(
84
+ "# FELA Grid Renewable\nProbabilistic solar and wind power forecasting that runs on a microcontroller at the station. Pick a track and a sample weather window to see the forecast range (P10 low, P50 median, P90 high), as a fraction of the site's rated capacity.\n\nPlay data is a small GEFCom2014-style sample of numerical weather prediction windows (public competition data, Hong et al. 2016). See the Space README."
85
+ )
86
+ with gr.Row():
87
+ track = gr.Radio(["solar", "wind"], value="solar", label="Track")
88
+ sample_idx = gr.Slider(0, 7, value=0, step=1, label="Sample weather window")
89
+ out = gr.Textbox(label="Forecast", lines=12)
90
+ btn = gr.Button("Forecast")
91
+ btn.click(forecast, inputs=[track, sample_idx], outputs=out)
92
+ track.change(forecast, inputs=[track, sample_idx], outputs=out)
93
+ sample_idx.change(forecast, inputs=[track, sample_idx], outputs=out)
94
+ if __name__ == "__main__":
95
+ demo.launch()
space/requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ torch>=2.0
2
+ numpy
3
+ gradio>=4.0
streaming/manifest.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model": "grid-renewable",
3
+ "format": "fp16-streaming",
4
+ "note": "load order is smallest-first for progressive/streaming load",
5
+ "files": [
6
+ {
7
+ "file": "model_fp16.safetensors",
8
+ "source": "model.safetensors",
9
+ "dtype": "fp16",
10
+ "bytes": 375928,
11
+ "approx_mb": 0.359
12
+ },
13
+ {
14
+ "file": "solar_fp16.safetensors",
15
+ "source": "solar.safetensors",
16
+ "dtype": "fp16",
17
+ "bytes": 375928,
18
+ "approx_mb": 0.359
19
+ },
20
+ {
21
+ "file": "wind_fp16.safetensors",
22
+ "source": "wind.safetensors",
23
+ "dtype": "fp16",
24
+ "bytes": 958228,
25
+ "approx_mb": 0.914
26
+ }
27
+ ]
28
+ }
streaming/model_fp16.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:03cb0a075cdd53dbaf9b1ccb54b29ed450d39b34f65a6cfb4dcd7cd58bd51baf
3
+ size 375928
streaming/solar_fp16.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:03cb0a075cdd53dbaf9b1ccb54b29ed450d39b34f65a6cfb4dcd7cd58bd51baf
3
+ size 375928
streaming/wind_fp16.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c98d362d96d949a64c9afbfcc0979813c2f9cbbefef54989cc093ea2ae2142bd
3
+ size 958228
train.py ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys, time, os, numpy as np, pandas as pd, torch, torch.nn as nn, torch.nn.functional as F
2
+
3
+ dev = "cuda" if torch.cuda.is_available() else "cpu"
4
+ torch.manual_seed(2024)
5
+ np.random.seed(2024)
6
+ if dev == "cpu" and os.environ.get("OMP_NUM_THREADS"):
7
+ torch.set_num_threads(int(os.environ["OMP_NUM_THREADS"]))
8
+ valflags = {"--save", "--arch", "--lr"}
9
+ pos = []
10
+ av = sys.argv[1:]
11
+ i = 0
12
+ while i < len(av):
13
+ if av[i] in valflags:
14
+ i += 2
15
+ continue
16
+ if av[i].startswith("--"):
17
+ i += 1
18
+ continue
19
+ pos.append(av[i])
20
+ i += 1
21
+ track = pos[0]
22
+ dL, dm, dD, dn = {"solar": (6, 3, 64, 4), "wind": (12, 6, 96, 3)}[track]
23
+ L = int(pos[1]) if len(pos) > 1 else dL
24
+ modes = int(pos[2]) if len(pos) > 2 else dm
25
+ D = int(pos[3]) if len(pos) > 3 else dD
26
+ nblk = int(pos[4]) if len(pos) > 4 else dn
27
+ ep = int(pos[5]) if len(pos) > 5 else 80
28
+ save = sys.argv[sys.argv.index("--save") + 1] if "--save" in sys.argv else None
29
+ arch = sys.argv[sys.argv.index("--arch") + 1] if "--arch" in sys.argv else "dual"
30
+ lr = float(sys.argv[sys.argv.index("--lr") + 1]) if "--lr" in sys.argv else 0.002
31
+ smoke = "--smoke" in sys.argv
32
+ prep = "/workspace/gefcom/prep"
33
+ qs = np.arange(1, 100) / 100.0
34
+ nwp = {
35
+ "solar": [
36
+ "VAR78",
37
+ "VAR79",
38
+ "VAR134",
39
+ "VAR157",
40
+ "VAR164",
41
+ "VAR165",
42
+ "VAR166",
43
+ "VAR167",
44
+ "VAR169",
45
+ "VAR175",
46
+ "VAR178",
47
+ "VAR228",
48
+ ],
49
+ "wind": ["U10", "V10", "U100", "V100"],
50
+ }[track]
51
+
52
+
53
+ def build():
54
+ df = (
55
+ pd.read_parquet(f"{prep}/{track}.parquet")
56
+ .sort_values(["ZONEID", "TIMESTAMP"])
57
+ .reset_index(drop=True)
58
+ )
59
+ df["hour"] = df.TIMESTAMP.dt.hour
60
+ df["doy"] = df.TIMESTAMP.dt.dayofyear
61
+ df["hsin"] = np.sin(2 * np.pi * df.hour / 24)
62
+ df["hcos"] = np.cos(2 * np.pi * df.hour / 24)
63
+ df["dsin"] = np.sin(2 * np.pi * df.doy / 365.25)
64
+ df["dcos"] = np.cos(2 * np.pi * df.doy / 365.25)
65
+ feats = list(nwp) + ["hsin", "hcos", "dsin", "dcos"]
66
+ if track == "wind":
67
+ df["ws10"] = np.hypot(df.U10, df.V10)
68
+ df["ws100"] = np.hypot(df.U100, df.V100)
69
+ df["wd100"] = np.arctan2(df.V100, df.U100)
70
+ df["wds"] = np.sin(df.wd100)
71
+ df["wdc"] = np.cos(df.wd100)
72
+ df["ws100_2"] = df.ws100**2
73
+ df["ws100_3"] = df.ws100**3
74
+ df["shear"] = df.ws100 - df.ws10
75
+ feats += ["ws10", "ws100", "wds", "wdc", "ws100_2", "ws100_3", "shear"]
76
+ else:
77
+ df["csi"] = df.VAR169 / (df.VAR178 + 1000.0)
78
+ df["cloud2"] = df.VAR164**2
79
+ df["temp_c"] = df.VAR167 - 273.15
80
+ df["daylight"] = (df.VAR178 > 10000.0).astype(np.float32)
81
+ feats += ["csi", "cloud2", "temp_c", "daylight"]
82
+ tr = df.is_test == 0
83
+ mu = df.loc[tr, feats].mean()
84
+ sd = df.loc[tr, feats].std() + 1e-06
85
+ df[feats] = (df[feats] - mu) / sd
86
+ df["y"] = df["y"].fillna(0.0)
87
+ X, Y, M = ([], [], [])
88
+ half = L // 2
89
+ for z, g in df.groupby("ZONEID"):
90
+ g = g.reset_index(drop=True)
91
+ fv = g[feats].values.astype(np.float32)
92
+ powr = g["y"].values.astype(np.float32)
93
+ iste = g["is_test"].values
94
+ T = len(g)
95
+ for i in range(T):
96
+ a = i - half
97
+ b = i - half + L
98
+ if a < 0 or b > T:
99
+ continue
100
+ X.append(fv[a:b])
101
+ Y.append(powr[i])
102
+ M.append(iste[i])
103
+ X = torch.tensor(np.array(X, dtype=np.float32))
104
+ Y = torch.tensor(np.array(Y, dtype=np.float32))
105
+ return (X, Y, np.array(M), df, feats)
106
+
107
+
108
+ class RevIN(nn.Module):
109
+ def __init__(self, C):
110
+ super().__init__()
111
+ self.g = nn.Parameter(torch.ones(C))
112
+ self.b = nn.Parameter(torch.zeros(C))
113
+
114
+ def norm(self, x):
115
+ self.m = x.mean(1, keepdim=True)
116
+ self.s = x.std(1, keepdim=True) + 1e-05
117
+ return (x - self.m) / self.s * self.g + self.b
118
+
119
+
120
+ class FNO1D(nn.Module):
121
+ def __init__(self, D, modes):
122
+ super().__init__()
123
+ self.modes = modes
124
+ s = 1 / (D * D)
125
+ self.w = nn.Parameter(s * torch.rand(modes, D, D, dtype=torch.cfloat))
126
+
127
+ def forward(self, x):
128
+ P = x.shape[1]
129
+ xf = torch.fft.rfft(x, dim=1)
130
+ m = min(self.modes, xf.shape[1])
131
+ o = torch.zeros_like(xf)
132
+ o[:, :m] = torch.einsum("bpd,pde->bpe", xf[:, :m], self.w[:m])
133
+ return torch.fft.irfft(o, n=P, dim=1)
134
+
135
+
136
+ class Block(nn.Module):
137
+ def __init__(self, D, modes, ff=2, drop=0.1):
138
+ super().__init__()
139
+ self.n1 = nn.LayerNorm(D)
140
+ self.fno = FNO1D(D, modes)
141
+ self.d1 = nn.Dropout(drop)
142
+ self.n2 = nn.LayerNorm(D)
143
+ self.ff = nn.Sequential(
144
+ nn.Linear(D, D * ff), nn.GELU(), nn.Dropout(drop), nn.Linear(D * ff, D)
145
+ )
146
+
147
+ def forward(self, x):
148
+ x = x + self.d1(self.fno(self.n1(x)))
149
+ return x + self.ff(self.n2(x))
150
+
151
+
152
+ class FELA_Grid(nn.Module):
153
+ def __init__(self, Fin, L, D=96, modes=6, nblk=3, nq=99, arch="dual"):
154
+ super().__init__()
155
+ self.L = L
156
+ self.arch = arch
157
+ self.center = L // 2
158
+ self.nq = nq
159
+ self.revin = RevIN(Fin)
160
+ self.embed = nn.Linear(Fin, D)
161
+ self.pos = nn.Parameter(0.02 * torch.randn(1, L, D))
162
+ self.blocks = nn.ModuleList([Block(D, modes) for _ in range(nblk)])
163
+ self.norm = nn.LayerNorm(D)
164
+ if arch == "dual":
165
+ self.direct = nn.Sequential(
166
+ nn.Linear(Fin, D), nn.GELU(), nn.Linear(D, D), nn.GELU()
167
+ )
168
+ fuse_in = 2 * D
169
+ else:
170
+ fuse_in = D
171
+ self.med = nn.Linear(fuse_in, 1)
172
+ self.spread = nn.Linear(fuse_in, nq)
173
+ self.register_buffer("qidx", torch.arange(nq))
174
+
175
+ def forward(self, x):
176
+ xc = x[:, self.center]
177
+ xn = self.revin.norm(x)
178
+ h = self.embed(xn) + self.pos
179
+ for b in self.blocks:
180
+ h = b(h)
181
+ ctx = self.norm(h)[:, self.center]
182
+ z = torch.cat([ctx, self.direct(xc)], dim=1) if self.arch == "dual" else ctx
183
+ med = torch.sigmoid(self.med(z))
184
+ w = F.softplus(self.spread(z))
185
+ half = self.nq // 2
186
+ below = -torch.flip(torch.cumsum(torch.flip(w[:, :half], [1]), 1), [1])
187
+ above = torch.cumsum(w[:, half + 1 :], 1)
188
+ offs = (
189
+ torch.cat([below, torch.zeros_like(w[:, half : half + 1]), above], dim=1)
190
+ * 0.05
191
+ )
192
+ return torch.clamp(med + offs, 0, 1)
193
+
194
+
195
+ def pinball(pred, y, qs):
196
+ y = y[:, None]
197
+ e = y - pred
198
+ return torch.maximum(qs[None, :] * e, (qs[None, :] - 1) * e).mean()
199
+
200
+
201
+ def main():
202
+ X, Y, M, df, feats = build()
203
+ Fin = X.shape[2]
204
+ tr_idx = np.where(M == 0)[0]
205
+ te_idx = np.where(M == 1)[0]
206
+ yte = Y[te_idx].numpy()
207
+ keep = ~np.isnan(yte)
208
+ te_idx = te_idx[keep]
209
+ nval = int(len(tr_idx) * 0.06)
210
+ va_idx = tr_idx[-nval:]
211
+ tr_idx = tr_idx[:-nval]
212
+ assert len(te_idx) == {"solar": 2154, "wind": 7390}[track]
213
+ if smoke:
214
+ print(
215
+ f"{track} Test_windows {len(te_idx)} train {len(tr_idx)} val {len(va_idx)} zones {df.ZONEID.nunique()}"
216
+ )
217
+ return
218
+ qst = torch.tensor(qs, dtype=torch.float32, device=dev)
219
+ Xtr, Ytr = (X[tr_idx].to(dev), Y[tr_idx].to(dev))
220
+ Xva, Yva = (X[va_idx].to(dev), Y[va_idx].to(dev))
221
+ Xte, Yte = (X[te_idx].to(dev), Y[te_idx].to(dev))
222
+ m = FELA_Grid(Fin, L, D=D, modes=modes, nblk=nblk, arch=arch).to(dev)
223
+ npar = sum((p.numel() for p in m.parameters()))
224
+ print(
225
+ f"[{track}] arch={arch} L={L} modes={modes} D={D} nblk={nblk} Fin={Fin} | train {len(Xtr)} val {len(Xva)} test {len(Xte)} | {npar / 1000.0:.0f}K"
226
+ )
227
+ opt = torch.optim.Adam(m.parameters(), lr=lr, weight_decay=1e-05)
228
+ sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, ep)
229
+ bs = 512
230
+
231
+ def ev(Xs, Ys):
232
+ m.eval()
233
+ with torch.no_grad():
234
+ ps = [m(Xs[i : i + 8192]) for i in range(0, len(Xs), 8192)]
235
+ p = torch.cat(ps)
236
+ return (pinball(p, Ys, qst).item(), p)
237
+
238
+ best = 1000000000.0
239
+ bstate = None
240
+ bad = 0
241
+ for e in range(ep):
242
+ m.train()
243
+ perm = torch.randperm(len(Xtr), device=dev)
244
+ for i in range(0, len(Xtr) - bs, bs):
245
+ idx = perm[i : i + bs]
246
+ loss = pinball(m(Xtr[idx]), Ytr[idx], qst)
247
+ opt.zero_grad()
248
+ loss.backward()
249
+ opt.step()
250
+ sched.step()
251
+ vpb, _ = ev(Xva, Yva)
252
+ if vpb < best:
253
+ best = vpb
254
+ bstate = {k: v.detach().clone() for k, v in m.state_dict().items()}
255
+ bad = 0
256
+ else:
257
+ bad += 1
258
+ if bad >= 15:
259
+ break
260
+ m.load_state_dict(bstate)
261
+ tpb, _ = ev(Xte, Yte)
262
+ bench = {"solar": 0.0285, "wind": 0.0792}[track]
263
+ print(
264
+ f"RESULT {track} pinball {tpb:.5f} (off.bench {bench}) | {npar / 1000.0:.0f}K"
265
+ )
266
+ if save:
267
+ torch.save(
268
+ {
269
+ "state": m.state_dict(),
270
+ "cfg": dict(Fin=Fin, L=L, D=D, modes=modes, nblk=nblk),
271
+ "feats": feats,
272
+ "track": track,
273
+ "test_pinball": tpb,
274
+ "off_bench": bench,
275
+ "npar": npar,
276
+ },
277
+ save,
278
+ )
279
+ print(f"SAVED {save}")
280
+
281
+
282
+ if __name__ == "__main__":
283
+ main()
verify.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import os
3
+ import sys
4
+ import torch
5
+
6
+ sys.path.insert(0, os.path.dirname(__file__))
7
+ from modeling import load_model
8
+
9
+ SHAPES = {"solar": (1, 6, 20), "wind": (1, 12, 15)}
10
+ VERIFICATION = {"solar": 1e-06, "wind": 0.446117}
11
+ TOL = 0.001
12
+
13
+
14
+ def fixed_input(track):
15
+ torch.manual_seed(0)
16
+ return torch.randn(*SHAPES[track])
17
+
18
+
19
+ def main():
20
+ ap = argparse.ArgumentParser()
21
+ ap.add_argument("--track", choices=["solar", "wind"], required=True)
22
+ ap.add_argument("--weights", default=".")
23
+ args = ap.parse_args()
24
+ model = load_model(args.weights, track=args.track)
25
+ x = fixed_input(args.track)
26
+ with torch.no_grad():
27
+ out = model(x)
28
+ if out.dim() != 2 or out.shape[0] != 1 or out.shape[-1] != 99:
29
+ print(f"Fail: unexpected output shape {tuple(out.shape)}, expected (1, 99)")
30
+ sys.exit(1)
31
+ p50 = out[0, 49].item()
32
+ print(f"Captured first-hour P50 for {args.track}: {p50:.6f}")
33
+ ref = VERIFICATION[args.track]
34
+ if abs(p50 - ref) > TOL:
35
+ print(
36
+ f"Fail: P50 {p50:.6f} differs from verification {ref:.6f} by more than {TOL}"
37
+ )
38
+ sys.exit(1)
39
+ print(f"Verification check OK (P50 within {TOL} of {ref:.6f})")
40
+ sys.exit(0)
41
+
42
+
43
+ if __name__ == "__main__":
44
+ main()
wind.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c0ad80ab6f8722e22deb64c964c75c9289f3e9c98f5a20654e09376963f217f2
3
+ size 1913168