pyroll-engineering-report 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ from .report import engineering_report
2
+ from .translations import LANGUAGES
3
+
4
+ VERSION = "3.0.0"
5
+
6
+ __all__ = ["engineering_report", "LANGUAGES", "VERSION"]
@@ -0,0 +1,321 @@
1
+ from datetime import datetime
2
+ from pathlib import Path
3
+ from typing import Callable, Optional
4
+
5
+ from fpdf import FPDF, FontFace
6
+ from pyroll.core import PassSequence, RollPass
7
+
8
+ from . import units
9
+ from .translations import LANGUAGES, translate
10
+
11
+ PAGE_WIDTH = 210.0
12
+ MARGIN = 10.0
13
+ CONTENT_WIDTH = PAGE_WIDTH - 2 * MARGIN
14
+ LOGO_WIDTH = 40.0
15
+ FONT = "Helvetica"
16
+ MISSING_VALUE = "-"
17
+
18
+
19
+ class EngineeringReportPdf(FPDF):
20
+ """A4 page layout with optional logo, company name and product name."""
21
+
22
+ def __init__(
23
+ self,
24
+ language: str,
25
+ generated_at: datetime,
26
+ product_name: Optional[str],
27
+ company_name: Optional[str],
28
+ mill_name: Optional[str],
29
+ logo: Optional[Path],
30
+ ):
31
+ super().__init__(orientation="P", unit="mm", format="A4")
32
+ self.language = language
33
+ self.generated_at = generated_at
34
+ self.product_name = product_name
35
+ self.company_name = company_name
36
+ self.mill_name = mill_name
37
+ self.logo = logo
38
+ self.set_margins(left=MARGIN, top=MARGIN, right=MARGIN)
39
+ self.set_auto_page_break(auto=True, margin=20)
40
+
41
+ def translate(self, key: str) -> str:
42
+ return translate(key, self.language)
43
+
44
+ def header(self):
45
+ top = MARGIN
46
+ if self.logo is not None:
47
+ image_info = self.image(str(self.logo), x=MARGIN, y=top, w=LOGO_WIDTH)
48
+ top += image_info.rendered_height + 3
49
+
50
+ self.set_xy(MARGIN, MARGIN)
51
+ self.set_font(FONT, size=9)
52
+ self.cell(CONTENT_WIDTH, 5, f"{self.translate('page')} {self.page_no()}/{{nb}}", align="R")
53
+
54
+ title = self.translate("roll_pass_design")
55
+ if self.company_name:
56
+ title = f"{self.company_name} - {title}"
57
+
58
+ self.set_xy(MARGIN, top)
59
+ self.set_font(FONT, style="B", size=12)
60
+ self.cell(CONTENT_WIDTH * 0.75, 6, title)
61
+ self.set_font(FONT, size=9)
62
+ self.cell(CONTENT_WIDTH * 0.25, 6, self.generated_at.strftime("%d.%m.%Y"), align="R",
63
+ new_x="LMARGIN", new_y="NEXT")
64
+
65
+ self.set_font(FONT, size=9)
66
+ if self.mill_name:
67
+ self.cell(CONTENT_WIDTH, 5, f"{self.translate('rolling_mill')}: {self.mill_name}", new_x="LMARGIN", new_y="NEXT")
68
+ if self.product_name:
69
+ self.cell(CONTENT_WIDTH, 5, f"{self.translate('product')}: {self.product_name}", new_x="LMARGIN", new_y="NEXT")
70
+
71
+ line_y = self.get_y() + 1.5
72
+ self.set_line_width(0.3)
73
+ self.line(MARGIN, line_y, PAGE_WIDTH - MARGIN, line_y)
74
+ self.set_y(line_y + 4)
75
+
76
+ def footer(self):
77
+ self.set_y(-16)
78
+ self.set_line_width(0.2)
79
+ self.line(MARGIN, self.get_y(), PAGE_WIDTH - MARGIN, self.get_y())
80
+ self.ln(2)
81
+
82
+ self.set_font(FONT, style="I", size=7)
83
+ self.set_text_color(110, 110, 110)
84
+ third = CONTENT_WIDTH / 3
85
+ self.cell(third, 4, f"{self.translate('generated')} {self.generated_at:%Y-%m-%d %H:%M:%S}")
86
+ self.cell(third, 4, self.company_name or "", align="C")
87
+ self.cell(third, 4, self.translate("generated_with"), align="R")
88
+ self.set_text_color(0, 0, 0)
89
+
90
+
91
+ def _format(value_getter: Callable[[], float], number_format: str) -> str:
92
+ """Formatted value of the getter or MISSING_VALUE if the value is not available."""
93
+ try:
94
+ value = value_getter()
95
+ except (AttributeError, TypeError, ValueError, ZeroDivisionError):
96
+ return MISSING_VALUE
97
+ if value is None:
98
+ return MISSING_VALUE
99
+ return format(value, number_format)
100
+
101
+
102
+ def _groove_label(roll_pass: RollPass) -> str:
103
+ label = getattr(roll_pass.roll.groove, "label", None)
104
+ return MISSING_VALUE if label in (None, "") else str(label)
105
+
106
+
107
+ def _reduction(roll_pass: RollPass) -> float:
108
+ in_area = roll_pass.in_profile.cross_section.area
109
+ return (in_area - roll_pass.out_profile.cross_section.area) / in_area
110
+
111
+
112
+ def _utilization(roll_pass: RollPass) -> float:
113
+ return roll_pass.engine.power / roll_pass.engine.maximum_power
114
+
115
+
116
+ def _material_name(profile) -> str:
117
+ material = getattr(profile, "material", None)
118
+ if not material:
119
+ return MISSING_VALUE
120
+ if isinstance(material, str):
121
+ return material
122
+ return str(list(material)[0])
123
+
124
+
125
+ def _mass_per_length(area: float, density: float) -> float:
126
+ return area * density
127
+
128
+
129
+ def _info_lines(
130
+ pdf: EngineeringReportPdf,
131
+ sequence: PassSequence,
132
+ max_heating_capacity: Optional[float],
133
+ nominal_area: Optional[float],
134
+ target_mass_deviation: Optional[float],
135
+ ) -> list[tuple[str, str]]:
136
+ in_profile = sequence.in_profile
137
+ out_profile = sequence.out_profile
138
+ last_pass = sequence.roll_passes[-1]
139
+ area_times_velocity = out_profile.cross_section.area * last_pass.out_profile.velocity
140
+ material_name = _material_name(in_profile)
141
+
142
+ lines = [
143
+ (pdf.translate("material"), material_name),
144
+ (pdf.translate("rolling_speed"), f"{_format(lambda: last_pass.out_profile.velocity, '.2f')} m/s"),
145
+ ("A × v", f"{_format(lambda: units.to_mm2(area_times_velocity), '.1f')} mm²·m/s"),
146
+ (
147
+ pdf.translate("in_profile_size"),
148
+ f"{_format(lambda: units.to_mm(in_profile.height), '.1f')} × "
149
+ f"{_format(lambda: units.to_mm(in_profile.width), '.1f')} mm",
150
+ ),
151
+ (pdf.translate("in_profile_temperature"), f"{_format(lambda: units.to_celsius(in_profile.temperature), '.0f')} °C"),
152
+ (
153
+ pdf.translate("production"),
154
+ f"{_format(lambda: units.to_tonnes_per_hour(area_times_velocity * in_profile.density), '.1f')} t/h",
155
+ ),
156
+ ]
157
+
158
+ if max_heating_capacity is not None:
159
+ lines.append((pdf.translate("max_heating_capacity"), f"{units.to_tonnes_per_hour(max_heating_capacity):.1f} t/h"))
160
+
161
+ if nominal_area is not None:
162
+ nominal_mass = _mass_per_length(nominal_area, in_profile.density)
163
+ lines.append((pdf.translate("nominal_mass"), f"{nominal_mass:.3f} kg/m"))
164
+ if target_mass_deviation is not None:
165
+ target_mass = nominal_mass * (1 + target_mass_deviation)
166
+ lines.append(
167
+ (pdf.translate("target_mass"), f"{target_mass:.3f} kg/m ({units.to_percent(target_mass_deviation):+.1f} %)")
168
+ )
169
+
170
+ return lines
171
+
172
+
173
+ def _write_info_block(pdf: EngineeringReportPdf, info_lines: list[tuple[str, str]]):
174
+ pdf.set_font(FONT, size=9)
175
+ for label, value in info_lines:
176
+ pdf.cell(55, 4.5, label)
177
+ pdf.cell(CONTENT_WIDTH - 55, 4.5, f": {value}", new_x="LMARGIN", new_y="NEXT")
178
+ pdf.ln(4)
179
+
180
+
181
+ def _write_table(
182
+ pdf: EngineeringReportPdf,
183
+ title_keys: list[str],
184
+ unit_labels: list[str],
185
+ rows: list[list[str]],
186
+ column_widths: list[float],
187
+ ):
188
+ pdf.set_font(FONT, size=7)
189
+ heading_style = FontFace(emphasis="BOLD", fill_color=(225, 225, 225))
190
+ with pdf.table(
191
+ col_widths=column_widths,
192
+ text_align="RIGHT",
193
+ num_heading_rows=2,
194
+ headings_style=heading_style,
195
+ line_height=4,
196
+ first_row_as_headings=True,
197
+ ) as table:
198
+ title_row = table.row()
199
+ for key in title_keys:
200
+ title_row.cell(pdf.translate(key), align="CENTER")
201
+ unit_row = table.row()
202
+ for unit_label in unit_labels:
203
+ unit_row.cell(unit_label, align="CENTER")
204
+ for values in rows:
205
+ table_row = table.row()
206
+ for value in values:
207
+ table_row.cell(value)
208
+
209
+
210
+ def _pass_schedule_rows(sequence: PassSequence) -> list[list[str]]:
211
+ rows = []
212
+ for roll_pass in sequence.roll_passes:
213
+ out_profile = roll_pass.out_profile
214
+ rows.append([
215
+ str(roll_pass.label),
216
+ _groove_label(roll_pass),
217
+ _format(lambda: units.to_mm(roll_pass.roll.nominal_diameter), ".1f"),
218
+ _format(lambda: units.to_mm(roll_pass.gap), ".2f"),
219
+ f"{_format(lambda: units.to_mm(out_profile.height), '.2f')} × "
220
+ f"{_format(lambda: units.to_mm(out_profile.width), '.2f')}",
221
+ _format(lambda: units.to_mm2(out_profile.cross_section.area), ".1f"),
222
+ _format(lambda: units.to_percent(out_profile.filling_ratio), ".1f"),
223
+ _format(lambda: units.to_percent(_reduction(roll_pass)), ".1f"),
224
+ _format(lambda: units.to_celsius(out_profile.temperature), ".0f"),
225
+ ])
226
+ return rows
227
+
228
+
229
+ def _speeds_and_loads_rows(sequence: PassSequence) -> list[list[str]]:
230
+ rows = []
231
+ for roll_pass in sequence.roll_passes:
232
+ roll = roll_pass.roll
233
+ engine = roll_pass.engine
234
+ rows.append([
235
+ str(roll_pass.label),
236
+ _groove_label(roll_pass),
237
+ _format(lambda: roll_pass.out_profile.velocity, ".2f"),
238
+ _format(lambda: units.to_mm(roll.working_diameter), ".1f"),
239
+ _format(lambda: units.to_per_minute(roll.rotational_frequency), ".1f"),
240
+ _format(lambda: engine.gear_ratio, ".3f"),
241
+ _format(lambda: units.to_per_minute(engine.rotational_frequency), ".0f"),
242
+ _format(lambda: roll_pass.elongation, ".3f"),
243
+ _format(lambda: units.to_mm(roll.nominal_diameter - roll.working_diameter), ".1f"),
244
+ _format(lambda: units.to_kilo(roll_pass.roll_force), ".1f"),
245
+ _format(lambda: units.to_kilo(2 * roll.roll_torque), ".2f"),
246
+ _format(lambda: units.to_kilo(engine.power), ".0f"),
247
+ _format(lambda: units.to_percent(_utilization(roll_pass)), ".1f"),
248
+ ])
249
+ return rows
250
+
251
+
252
+ def _total_power(sequence: PassSequence) -> Optional[float]:
253
+ try:
254
+ return sum(roll_pass.engine.power for roll_pass in sequence.roll_passes)
255
+ except (AttributeError, TypeError):
256
+ return None
257
+
258
+
259
+ def engineering_report(
260
+ sequence: PassSequence,
261
+ output_path: Path,
262
+ language: str = "en",
263
+ product_name: Optional[str] = None,
264
+ company_name: Optional[str] = None,
265
+ mill_name: Optional[str] = None,
266
+ logo: Optional[Path] = None,
267
+ max_heating_capacity: Optional[float] = None,
268
+ nominal_area: Optional[float] = None,
269
+ target_mass_deviation: Optional[float] = None,
270
+ ) -> Path:
271
+ """Writes the engineering report of a solved pass sequence as PDF and returns its path."""
272
+ if language not in LANGUAGES:
273
+ raise ValueError(f"Language '{language}' not available, choose one of {LANGUAGES}.")
274
+
275
+ pdf = EngineeringReportPdf(
276
+ language=language,
277
+ generated_at=datetime.now(),
278
+ product_name=product_name,
279
+ company_name=company_name,
280
+ mill_name=mill_name,
281
+ logo=Path(logo) if logo is not None else None,
282
+ )
283
+ info_lines = _info_lines(pdf, sequence, max_heating_capacity, nominal_area, target_mass_deviation)
284
+
285
+ pdf.add_page()
286
+ _write_info_block(pdf, info_lines)
287
+ pdf.set_font(FONT, style="B", size=10)
288
+ pdf.cell(CONTENT_WIDTH, 6, pdf.translate("pass_schedule"), new_x="LMARGIN", new_y="NEXT")
289
+ _write_table(
290
+ pdf,
291
+ title_keys=["stand", "groove", "roll_diameter", "gap", "height_x_width", "area", "filling_ratio",
292
+ "reduction", "temperature"],
293
+ unit_labels=["", "", "mm", "mm", "mm", "mm²", "%", "%", "°C"],
294
+ rows=_pass_schedule_rows(sequence),
295
+ column_widths=[16, 14, 20, 16, 34, 22, 22, 22, 24],
296
+ )
297
+
298
+ pdf.add_page()
299
+ _write_info_block(pdf, info_lines)
300
+ pdf.set_font(FONT, style="B", size=10)
301
+ pdf.cell(CONTENT_WIDTH, 6, pdf.translate("speeds_and_loads"), new_x="LMARGIN", new_y="NEXT")
302
+ _write_table(
303
+ pdf,
304
+ title_keys=["stand", "groove", "speed", "working_diameter", "roll_rotational_frequency", "gear_ratio",
305
+ "motor_rotational_frequency", "elongation", "groove_factor", "roll_force", "roll_torque",
306
+ "power", "utilization"],
307
+ unit_labels=["", "", "m/s", "mm", "1/min", "", "1/min", "", "mm", "kN", "kNm", "kW", "%"],
308
+ rows=_speeds_and_loads_rows(sequence),
309
+ column_widths=[14, 12, 14, 15, 16, 15, 15, 19, 14, 15, 14, 14, 12],
310
+ )
311
+
312
+ total_power = _total_power(sequence)
313
+ pdf.ln(3)
314
+ pdf.set_font(FONT, style="B", size=9)
315
+ total_power_text = MISSING_VALUE if total_power is None else f"{units.to_kilo(total_power):.0f}"
316
+ pdf.cell(CONTENT_WIDTH, 5, f"{pdf.translate('total_power')}: {total_power_text} kW", align="R")
317
+
318
+ output_path = Path(output_path)
319
+ output_path.parent.mkdir(parents=True, exist_ok=True)
320
+ pdf.output(str(output_path))
321
+ return output_path
@@ -0,0 +1,49 @@
1
+ LANGUAGES = ("de", "en", "it")
2
+
3
+ TEXTS = {
4
+ "roll_pass_design": {"de": "Walzenkalibrierung", "en": "Roll pass design", "it": "Calibratura dei cilindri"},
5
+ "rolling_mill": {"de": "Walzstraße", "en": "Rolling mill", "it": "Laminatoio"},
6
+ "product": {"de": "Produkt", "en": "Product", "it": "Prodotto"},
7
+ "page": {"de": "Seite", "en": "Page", "it": "Pagina"},
8
+ "generated": {"de": "Erstellt", "en": "Generated", "it": "Generato"},
9
+ "generated_with": {"de": "Erstellt mit PyRolL", "en": "Generated with PyRolL", "it": "Generato con PyRolL"},
10
+ "pass_schedule": {"de": "Stichplan", "en": "Pass schedule", "it": "Schema di laminazione"},
11
+ "speeds_and_loads": {
12
+ "de": "Geschwindigkeiten und Belastungen",
13
+ "en": "Speeds and loads",
14
+ "it": "Velocità e carichi",
15
+ },
16
+ "material": {"de": "Werkstoff", "en": "Material", "it": "Materiale"},
17
+ "rolling_speed": {"de": "Walzgeschwindigkeit", "en": "Rolling speed", "it": "Velocità di laminazione"},
18
+ "in_profile_size": {"de": "Knüppelabmessung", "en": "Billet size", "it": "Dimensione billetta"},
19
+ "in_profile_temperature": {"de": "Knüppeltemperatur", "en": "Billet temperature", "it": "Temperatura billetta"},
20
+ "max_heating_capacity": {"de": "Max. Ofenleistung", "en": "Max. heating capacity", "it": "Capacità max. forno"},
21
+ "production": {"de": "Produktionsleistung", "en": "Production", "it": "Produzione"},
22
+ "nominal_mass": {"de": "Nennmasse", "en": "Nominal mass", "it": "Massa nominale"},
23
+ "target_mass": {"de": "Zielmasse", "en": "Target mass", "it": "Massa obiettivo"},
24
+ "total_power": {"de": "Gesamte Walzleistung", "en": "Total rolling power", "it": "Potenza totale di laminazione"},
25
+ "stand": {"de": "Gerüst", "en": "Stand", "it": "Gabbia"},
26
+ "groove": {"de": "Kaliber", "en": "Groove", "it": "Calibro"},
27
+ "roll_diameter": {"de": "Walzen-Ø", "en": "Roll dia.", "it": "Ø cilindro"},
28
+ "gap": {"de": "Spalt", "en": "Gap", "it": "Luce"},
29
+ "height_x_width": {"de": "Höhe × Breite", "en": "Height × width", "it": "Altezza × larghezza"},
30
+ "area": {"de": "Fläche", "en": "Area", "it": "Area"},
31
+ "filling_ratio": {"de": "Füllgrad", "en": "Filling ratio", "it": "Riempimento"},
32
+ "reduction": {"de": "Abnahme", "en": "Reduction", "it": "Riduzione"},
33
+ "temperature": {"de": "Temperatur", "en": "Temperature", "it": "Temperatura"},
34
+ "speed": {"de": "Geschw.", "en": "Speed", "it": "Velocità"},
35
+ "working_diameter": {"de": "Arbeits-Ø", "en": "Work. dia.", "it": "Ø lavoro"},
36
+ "roll_rotational_frequency": {"de": "Walzendrehzahl", "en": "Roll speed", "it": "Giri cilindro"},
37
+ "gear_ratio": {"de": "Übersetzung", "en": "Gear ratio", "it": "Rapporto riduzione"},
38
+ "motor_rotational_frequency": {"de": "Motordrehzahl", "en": "Motor speed", "it": "Giri motore"},
39
+ "elongation": {"de": "Streckung", "en": "Elongation", "it": "Allungamento"},
40
+ "groove_factor": {"de": "Kaliberfaktor", "en": "Groove factor", "it": "Fattore calibro"},
41
+ "roll_force": {"de": "Walzkraft", "en": "Roll force", "it": "Forza"},
42
+ "roll_torque": {"de": "Walzmoment", "en": "Roll torque", "it": "Coppia"},
43
+ "power": {"de": "Leistung", "en": "Power", "it": "Potenza"},
44
+ "utilization": {"de": "Auslastung", "en": "Utilization", "it": "Utilizzo"},
45
+ }
46
+
47
+
48
+ def translate(key: str, language: str) -> str:
49
+ return TEXTS[key][language]
@@ -0,0 +1,31 @@
1
+ """Conversion of SI values into the units printed in the report."""
2
+
3
+ KELVIN_OFFSET = 273.15
4
+
5
+
6
+ def to_mm(value_m: float) -> float:
7
+ return value_m * 1e3
8
+
9
+
10
+ def to_mm2(value_m2: float) -> float:
11
+ return value_m2 * 1e6
12
+
13
+
14
+ def to_celsius(value_kelvin: float) -> float:
15
+ return value_kelvin - KELVIN_OFFSET
16
+
17
+
18
+ def to_kilo(value: float) -> float:
19
+ return value * 1e-3
20
+
21
+
22
+ def to_percent(ratio: float) -> float:
23
+ return ratio * 1e2
24
+
25
+
26
+ def to_per_minute(frequency_per_second: float) -> float:
27
+ return frequency_per_second * 60
28
+
29
+
30
+ def to_tonnes_per_hour(mass_flow_kg_per_s: float) -> float:
31
+ return mass_flow_kg_per_s * 3.6
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.5
2
+ Name: pyroll-engineering-report
3
+ Version: 3.0.0
4
+ Summary: Plugin for PyRolL providing a printable PDF engineering report (pass schedule, speeds and loads) of a solved pass sequence.
5
+ Project-URL: Homepage, https://pyroll.readthedocs.io
6
+ Project-URL: Repository, https://codeberg.org/pyroll-project/pyroll-engineering-report
7
+ Project-URL: Documentation, https://codeberg.org/pyroll-project/pyroll-engineering-report/src/branch/main/docs/docs.pdf
8
+ Author-email: Christoph Renzing <christoph.renzing@imf.tu-freiberg.de>
9
+ License-Expression: BSD-3-Clause
10
+ License-File: LICENSE
11
+ Requires-Python: >=3.11
12
+ Requires-Dist: fpdf2>=2.8
13
+ Requires-Dist: pyroll-core<4.0,>=3.1
@@ -0,0 +1,8 @@
1
+ pyroll/engineering_report/__init__.py,sha256=p4emyjz1ZZxHbOsSq9iK3K9Ur3ZEE4kd27dKhwgAFP0,152
2
+ pyroll/engineering_report/report.py,sha256=tSUk2DUvBFYEffK0bJlkExr1KC1kAFsil3LZWfJMbBQ,12496
3
+ pyroll/engineering_report/translations.py,sha256=zUoQ_AhMDoIy5y8kHWRD1Qhs_1v9Ke1jdzvQU9CsSgg,3380
4
+ pyroll/engineering_report/units.py,sha256=H7C4GIFulTdytBWR9SO9pK-nxdCgmWEsMzEyT8lgUQM,629
5
+ pyroll_engineering_report-3.0.0.dist-info/METADATA,sha256=v9DqhOYOBakfO6soJqUoqw_w18vmuZ-pph-nGwndFkY,675
6
+ pyroll_engineering_report-3.0.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
7
+ pyroll_engineering_report-3.0.0.dist-info/licenses/LICENSE,sha256=ba04C9xPoD_SK1gQPi-rnym3Kktrfbe0dWCbqnsw_Mk,1448
8
+ pyroll_engineering_report-3.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,11 @@
1
+ Copyright 2022 ...
2
+
3
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4
+
5
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6
+
7
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8
+
9
+ 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
10
+
11
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.