pyroll-engineering-report 3.0.0__tar.gz

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,435 @@
1
+ ## Python
2
+
3
+ .working
4
+ working
5
+ *.lock
6
+ .pytask.sqlite3
7
+
8
+ # Byte-compiled / optimized / DLL files
9
+ __pycache__/
10
+ *.py[cod]
11
+ *$py.class
12
+
13
+ # C extensions
14
+ *.so
15
+
16
+ # Distribution / packaging
17
+ .Python
18
+ build/
19
+ develop-eggs/
20
+ dist/
21
+ downloads/
22
+ eggs/
23
+ .eggs/
24
+ lib/
25
+ lib64/
26
+ parts/
27
+ sdist/
28
+ var/
29
+ wheels/
30
+ share/python-wheels/
31
+ *.egg-info/
32
+ .installed.cfg
33
+ *.egg
34
+ MANIFEST
35
+
36
+ # PyInstaller
37
+ # Usually these files are written by a python script from a template
38
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
39
+ *.manifest
40
+ *.spec
41
+
42
+ # Installer logs
43
+ pip-log.txt
44
+ pip-delete-this-directory.txt
45
+
46
+ # Unit test / coverage reports
47
+ htmlcov/
48
+ .tox/
49
+ .nox/
50
+ .coverage
51
+ .coverage.*
52
+ .cache
53
+ nosetests.xml
54
+ coverage.xml
55
+ *.cover
56
+ *.py,cover
57
+ .hypothesis/
58
+ .pytest_cache/
59
+ cover/
60
+
61
+ # Translations
62
+ *.mo
63
+ *.pot
64
+
65
+ # Django stuff:
66
+ *.log
67
+ local_settings.py
68
+ db.sqlite3
69
+ db.sqlite3-journal
70
+
71
+ # Flask stuff:
72
+ instance/
73
+ .webassets-cache
74
+
75
+ # Scrapy stuff:
76
+ .scrapy
77
+
78
+ # Sphinx documentation
79
+ docs/_build/
80
+
81
+ # PyBuilder
82
+ .pybuilder/
83
+ target/
84
+
85
+ # Jupyter Notebook
86
+ .ipynb_checkpoints
87
+
88
+ # IPython
89
+ profile_default/
90
+ ipython_config.py
91
+
92
+ # pyenv
93
+ # For a library or package, you might want to ignore these files since the code is
94
+ # intended to run in multiple environments; otherwise, check them in:
95
+ # .python-version
96
+
97
+ # pipenv
98
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
99
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
100
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
101
+ # install all needed dependencies.
102
+ #Pipfile.lock
103
+
104
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
105
+ __pypackages__/
106
+
107
+ # Celery stuff
108
+ celerybeat-schedule
109
+ celerybeat.pid
110
+
111
+ # SageMath parsed files
112
+ *.sage.py
113
+
114
+ # Environments
115
+ .env
116
+ .venv
117
+ env/
118
+ venv/
119
+ ENV/
120
+ env.bak/
121
+ venv.bak/
122
+
123
+ # Spyder project settings
124
+ .spyderproject
125
+ .spyproject
126
+
127
+ # Rope project settings
128
+ .ropeproject
129
+
130
+ # mkdocs documentation
131
+ /site
132
+
133
+ # mypy
134
+ .mypy_cache/
135
+ .dmypy.json
136
+ dmypy.json
137
+
138
+ # Pyre type checker
139
+ .pyre/
140
+
141
+ # pytype static type analyzer
142
+ .pytype/
143
+
144
+ # Cython debug symbols
145
+ cython_debug/
146
+
147
+ ## TeX
148
+
149
+ ## Core latex/pdflatex auxiliary files:
150
+ *.aux
151
+ *.lof
152
+ *.lot
153
+ *.fls
154
+ *.out
155
+ *.toc
156
+ *.fmt
157
+ *.fot
158
+ *.cb
159
+ *.cb2
160
+ .*.lb
161
+
162
+ ## Intermediate documents:
163
+ *.dvi
164
+ *.xdv
165
+ *-converted-to.*
166
+ # these rules might exclude image files for figures etc.
167
+ # *.ps
168
+ # *.eps
169
+ # *.pdf
170
+
171
+ ## Generated if empty string is given at "Please type another file name for output:"
172
+ .pdf
173
+
174
+ ## Bibliography auxiliary files (bibtex/biblatex/biber):
175
+ *.bbl
176
+ *.bcf
177
+ *.blg
178
+ *-blx.aux
179
+ *-blx.bib
180
+ *.run.xml
181
+
182
+ ## Build tool auxiliary files:
183
+ *.fdb_latexmk
184
+ *.synctex
185
+ *.synctex(busy)
186
+ *.synctex.gz
187
+ *.synctex.gz(busy)
188
+ *.pdfsync
189
+
190
+ ## Build tool directories for auxiliary files
191
+ # latexrun
192
+ latex.out/
193
+
194
+ ## Auxiliary and intermediate files from other packages:
195
+ # algorithms
196
+ *.alg
197
+ *.loa
198
+
199
+ # achemso
200
+ acs-*.bib
201
+
202
+ # amsthm
203
+ *.thm
204
+
205
+ # beamer
206
+ *.nav
207
+ *.pre
208
+ *.snm
209
+ *.vrb
210
+
211
+ # changes
212
+ *.soc
213
+
214
+ # comment
215
+ *.cut
216
+
217
+ # cprotect
218
+ *.cpt
219
+
220
+ # elsarticle (documentclass of Elsevier journals)
221
+ *.spl
222
+
223
+ # endnotes
224
+ *.ent
225
+
226
+ *.lox
227
+
228
+ # feynmf/feynmp
229
+ *.mf
230
+ *.mp
231
+ *.t[1-9]
232
+ *.t[1-9][0-9]
233
+ *.tfm
234
+
235
+ #(r)(e)ledmac/(r)(e)ledpar
236
+ *.end
237
+ *.?end
238
+ *.[1-9]
239
+ *.[1-9][0-9]
240
+ *.[1-9][0-9][0-9]
241
+ *.[1-9]R
242
+ *.[1-9][0-9]R
243
+ *.[1-9][0-9][0-9]R
244
+ *.eledsec[1-9]
245
+ *.eledsec[1-9]R
246
+ *.eledsec[1-9][0-9]
247
+ *.eledsec[1-9][0-9]R
248
+ *.eledsec[1-9][0-9][0-9]
249
+ *.eledsec[1-9][0-9][0-9]R
250
+
251
+ # glossaries
252
+ *.acn
253
+ *.acr
254
+ *.glg
255
+ *.glo
256
+ *.gls
257
+ *.glsdefs
258
+ *.lzo
259
+ *.lzs
260
+
261
+ # uncomment this for glossaries-extra (will ignore makeindex's style files!)
262
+ # *.ist
263
+
264
+ # gnuplottex
265
+ *-gnuplottex-*
266
+
267
+ # gregoriotex
268
+ *.gaux
269
+ *.glog
270
+ *.gtex
271
+
272
+ # htlatex
273
+ *.4ct
274
+ *.4tc
275
+ *.idv
276
+ *.lg
277
+ *.trc
278
+ *.xref
279
+
280
+ # hyperref
281
+ *.brf
282
+
283
+ # knitr
284
+ *-concordance.tex
285
+ # *.tikz
286
+ *-tikzDictionary
287
+
288
+ # listings
289
+ *.lol
290
+
291
+ # luatexja-ruby
292
+ *.ltjruby
293
+
294
+ # makeidx
295
+ *.idx
296
+ *.ilg
297
+ *.ind
298
+
299
+ # minitoc
300
+ *.maf
301
+ *.mlf
302
+ *.mlt
303
+ *.mtc[0-9]*
304
+ *.slf[0-9]*
305
+ *.slt[0-9]*
306
+ *.stc[0-9]*
307
+
308
+ # minted
309
+ _minted*
310
+ *.pyg
311
+
312
+ # morewrites
313
+ *.mw
314
+
315
+ # newpax
316
+ *.newpax
317
+
318
+ # nomencl
319
+ *.nlg
320
+ *.nlo
321
+ *.nls
322
+
323
+ # pax
324
+ *.pax
325
+
326
+ # pdfpcnotes
327
+ *.pdfpc
328
+
329
+ # sagetex
330
+ *.sagetex.sage
331
+ *.sagetex.py
332
+ *.sagetex.scmd
333
+
334
+ # scrwfile
335
+ *.wrt
336
+
337
+ # sympy
338
+ *.sout
339
+ *.sympy
340
+ sympy-plots-for-*.tex/
341
+
342
+ # pdfcomment
343
+ *.upa
344
+ *.upb
345
+
346
+ # pythontex
347
+ *.pytxcode
348
+ pythontex-files-*/
349
+
350
+ # tcolorbox
351
+ *.listing
352
+
353
+ # thmtools
354
+ *.loe
355
+
356
+ # TikZ & PGF
357
+ *.dpth
358
+ *.md5
359
+ *.auxlock
360
+
361
+ # todonotes
362
+ *.tdo
363
+
364
+ # vhistory
365
+ *.hst
366
+ *.ver
367
+
368
+ # easy
369
+ *.lod
370
+
371
+ # xcolor
372
+ *.xcp
373
+
374
+ # xmpincl
375
+ *.xmpi
376
+
377
+ # xindy
378
+ *.xdy
379
+
380
+ # xypic precompiled matrices and outlines
381
+ *.xyc
382
+ *.xyd
383
+
384
+ # endfloat
385
+ *.ttt
386
+ *.fff
387
+
388
+ # Latexian
389
+ TSWLatexianTemp*
390
+
391
+ ## Editors:
392
+ # WinEdt
393
+ *.bak
394
+ *.sav
395
+
396
+ # Texpad
397
+ .texpadtmp
398
+
399
+ # LyX
400
+ *.lyx~
401
+
402
+ # Kile
403
+ *.backup
404
+
405
+ # gummi
406
+ .*.swp
407
+
408
+ # KBibTeX
409
+ *~[0-9]*
410
+
411
+ # TeXnicCenter
412
+ *.tps
413
+
414
+ # auto folder when using emacs and auctex
415
+ ./auto/*
416
+ *.el
417
+
418
+ # expex forward references with \gathertags
419
+ *-tags.tex
420
+
421
+ # standalone packages
422
+ *.sta
423
+
424
+ # Makeindex log files
425
+ *.lpz
426
+
427
+ # xwatermark package
428
+ *.xwm
429
+
430
+ ## JetBrains
431
+
432
+ .idea
433
+
434
+ ## HTML report
435
+ /out/report.html
@@ -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.
@@ -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,40 @@
1
+ [build]
2
+ packages = [
3
+ "pyroll"
4
+ ]
5
+
6
+ [version]
7
+ path = "pyroll/engineering_report/__init__.py"
8
+
9
+ [envs.default]
10
+ path = ".venv"
11
+ dependencies = [
12
+ "pytest ~= 7.0",
13
+ "pypdf",
14
+ "pyroll-wusatowski-spreading ~= 3.0",
15
+ "pyroll-hensel-force-torque ~= 3.0",
16
+ ]
17
+
18
+ [envs.docs]
19
+ path = ""
20
+ dependencies = [
21
+ "pygments ~= 2.0"
22
+ ]
23
+
24
+ [envs.docs.scripts]
25
+ build = "latexmk --cd --pdf --shell-escape docs/docs"
26
+
27
+ [envs.test]
28
+ path = ""
29
+ dependencies = [
30
+ "pytest ~= 7.0",
31
+ "pypdf",
32
+ "pyroll-wusatowski-spreading ~= 3.0",
33
+ "pyroll-hensel-force-torque ~= 3.0",
34
+ ]
35
+
36
+ [envs.test.scripts]
37
+ all = "pytest"
38
+
39
+ [[envs.test.matrix]]
40
+ python = ["3.11", "3.12", "3.13"]
@@ -0,0 +1,23 @@
1
+ [project]
2
+ name = "pyroll-engineering-report"
3
+ description = "Plugin for PyRolL providing a printable PDF engineering report (pass schedule, speeds and loads) of a solved pass sequence."
4
+ authors = [
5
+ { name = "Christoph Renzing", email = "christoph.renzing@imf.tu-freiberg.de" }
6
+ ]
7
+ license = "BSD-3-Clause"
8
+ requires-python = ">=3.11"
9
+ dependencies = [
10
+ "pyroll-core >= 3.1, < 4.0",
11
+ "fpdf2 >= 2.8",
12
+ ]
13
+
14
+ dynamic = ["version"]
15
+
16
+ [project.urls]
17
+ Homepage = "https://pyroll.readthedocs.io"
18
+ Repository = "https://codeberg.org/pyroll-project/pyroll-engineering-report"
19
+ Documentation = "https://codeberg.org/pyroll-project/pyroll-engineering-report/src/branch/main/docs/docs.pdf"
20
+
21
+ [build-system]
22
+ requires = ["hatchling"]
23
+ build-backend = "hatchling.build"
@@ -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