pyvcad-attribute-resolver 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.
Files changed (31) hide show
  1. pyvcad_attribute_resolver/__init__.py +25 -0
  2. pyvcad_attribute_resolver/adapt.py +52 -0
  3. pyvcad_attribute_resolver/conversion_entry.py +35 -0
  4. pyvcad_attribute_resolver/conversion_graph.py +58 -0
  5. pyvcad_attribute_resolver/modules/__init__.py +14 -0
  6. pyvcad_attribute_resolver/modules/foaming/.gitignore +1 -0
  7. pyvcad_attribute_resolver/modules/foaming/__init__.py +110 -0
  8. pyvcad_attribute_resolver/modules/foaming/data.py +256 -0
  9. pyvcad_attribute_resolver/modules/foaming/plot_data.py +397 -0
  10. pyvcad_attribute_resolver/modules/generic_color/__init__.py +69 -0
  11. pyvcad_attribute_resolver/modules/generic_color/data.py +6 -0
  12. pyvcad_attribute_resolver/modules/j750_modulus_toughness/__init__.py +153 -0
  13. pyvcad_attribute_resolver/modules/j750_modulus_toughness/__marimo__/session/j750_modulus_toughness_companion.py.json +269 -0
  14. pyvcad_attribute_resolver/modules/j750_modulus_toughness/data.py +646 -0
  15. pyvcad_attribute_resolver/modules/j750_modulus_toughness/j750_modulus_toughness_companion.py +677 -0
  16. pyvcad_attribute_resolver/modules/j750_modulus_toughness/j750_modulus_toughness_lut.json +99252 -0
  17. pyvcad_attribute_resolver/modules/j750_modulus_toughness/legacy_characterization.json +2908 -0
  18. pyvcad_attribute_resolver/modules/j750_modulus_toughness/notebook_support.py +1567 -0
  19. pyvcad_attribute_resolver/modules/j750_shore_hardness/__init__.py +111 -0
  20. pyvcad_attribute_resolver/modules/j750_shore_hardness/data.py +53 -0
  21. pyvcad_attribute_resolver/modules/j750_shore_hardness/fit_curve.py +344 -0
  22. pyvcad_attribute_resolver/modules/j750_shore_hardness/fitted_model.py +167 -0
  23. pyvcad_attribute_resolver/modules/j750_shore_hardness/j750_shore_hardness_data.csv +21 -0
  24. pyvcad_attribute_resolver/modules/stratasys_radiomatrix/__init__.py +182 -0
  25. pyvcad_attribute_resolver/modules/stratasys_radiomatrix/data.py +170 -0
  26. pyvcad_attribute_resolver/registry.py +48 -0
  27. pyvcad_attribute_resolver/solver.py +126 -0
  28. pyvcad_attribute_resolver-3.0.0.dist-info/METADATA +497 -0
  29. pyvcad_attribute_resolver-3.0.0.dist-info/RECORD +31 -0
  30. pyvcad_attribute_resolver-3.0.0.dist-info/WHEEL +4 -0
  31. pyvcad_attribute_resolver-3.0.0.dist-info/licenses/LICENSE +185 -0
@@ -0,0 +1,25 @@
1
+ from .registry import (register_conversion, unregister_conversion,
2
+ list_conversions, clear_conversions)
3
+ from .adapt import adapt
4
+ from .solver import ResolverError, NoPathError, AmbiguousPathError
5
+ from .conversion_entry import ConversionEntry
6
+ from .modules.foaming import (register_foaming_conversions,
7
+ register_tpu_conversions,
8
+ register_pla_conversions)
9
+ from .modules.j750_shore_hardness import (
10
+ fit_j750_shore_hardness_curve,
11
+ generate_j750_shore_hardness_fit_plot,
12
+ register_j750_shore_hardness_conversions,
13
+ )
14
+ from .modules.j750_modulus_toughness import (
15
+ register_j750_modulus_toughness_conversions,
16
+ generate_j750_modulus_toughness_lookup_table,
17
+ generate_j750_modulus_toughness_reachable_region_plot,
18
+ )
19
+ from .modules.stratasys_radiomatrix import (
20
+ register_stratasys_radiomatrix_conversions,
21
+ )
22
+ from .modules.generic_color import (register_generic_color_conversions,
23
+ register_rgb_to_rgba_conversion,
24
+ register_rgba_to_rgb_conversion)
25
+ from .modules import list_modules
@@ -0,0 +1,52 @@
1
+ import pyvcad as pv
2
+ from .registry import _global_graph
3
+ from .solver import solve
4
+
5
+
6
+ def adapt(design, target_attributes,
7
+ allowed_conversions=None, tags=None, max_depth=10):
8
+ """Wrap a design node with AttributeModifiers to produce target attributes.
9
+
10
+ For each target attribute not already present on the design, the
11
+ solver finds a conversion path through the registered graph and
12
+ wraps the design in the corresponding ``AttributeModifier`` chain.
13
+
14
+ Targets are processed sequentially so that attributes produced by
15
+ earlier conversions are available for later ones.
16
+
17
+ Arguments:
18
+ design: ``pyvcad.Node``, the root of the design tree.
19
+ target_attributes: List of attribute name strings the caller
20
+ needs on the output.
21
+ allowed_conversions: Optional list of ``ConversionEntry`` names
22
+ to restrict the solver.
23
+ tags: Optional list of tags to filter conversions.
24
+ max_depth: Maximum chain length per target (default 10).
25
+
26
+ Returns:
27
+ ``pyvcad.Node`` — the original design wrapped in zero or more
28
+ ``AttributeModifier`` nodes. Returns the design unchanged if
29
+ all targets are already present.
30
+
31
+ Raises:
32
+ NoPathError: If any target attribute cannot be reached.
33
+ AmbiguousPathError: If disambiguation fails for any target.
34
+ """
35
+ available = design.attribute_list()
36
+ result = design
37
+
38
+ for target in target_attributes:
39
+ if target in available:
40
+ continue
41
+
42
+ path = solve(_global_graph, available, target,
43
+ allowed_conversions=allowed_conversions,
44
+ tags=tags, max_depth=max_depth)
45
+
46
+ for entry in path:
47
+ converter = entry.converter_factory()
48
+ result = pv.AttributeModifier(converter, result)
49
+
50
+ available = result.attribute_list()
51
+
52
+ return result
@@ -0,0 +1,35 @@
1
+ class ConversionEntry:
2
+ """A registered conversion between two attribute types.
3
+
4
+ Each entry represents a directed edge in the conversion graph, from a
5
+ source attribute to a target attribute. The ``converter_factory`` callable
6
+ produces a fresh ``AttributeConverterBase`` instance each time it is
7
+ invoked, supporting clone-safe, thread-safe parallel evaluation.
8
+
9
+ Attributes:
10
+ source: Input attribute name.
11
+ target: Output attribute name.
12
+ converter_factory: Zero-argument callable returning an
13
+ ``AttributeConverterBase``.
14
+ name: Human-readable identifier for this conversion.
15
+ priority: Higher values are preferred when disambiguating
16
+ multiple equally-short paths.
17
+ tags: Context-filtering labels (e.g. ``"foaming_tpu"``).
18
+ required_inputs: Attribute names that must already be present on the
19
+ design before this conversion can be applied.
20
+ """
21
+
22
+ def __init__(self, source, target, converter_factory,
23
+ name=None, priority=0, tags=None, required_inputs=None):
24
+ self.source = source
25
+ self.target = target
26
+ self.converter_factory = converter_factory
27
+ self.name = name or f"{source}_to_{target}"
28
+ self.priority = priority
29
+ self.tags = tags or []
30
+ self.required_inputs = list(required_inputs) if required_inputs else [source]
31
+
32
+ def __repr__(self):
33
+ return (f"ConversionEntry('{self.source}' -> '{self.target}', "
34
+ f"name='{self.name}', priority={self.priority}, "
35
+ f"required_inputs={self.required_inputs})")
@@ -0,0 +1,58 @@
1
+ class ConversionGraph:
2
+ """Directed graph of attribute conversions.
3
+
4
+ Vertices are attribute name strings. Edges are ``ConversionEntry``
5
+ objects representing registered conversions from one attribute to
6
+ another. Implemented as a pure adjacency-list with no external
7
+ dependencies.
8
+ """
9
+
10
+ def __init__(self):
11
+ self._adjacency = {} # dict[str, list[ConversionEntry]]
12
+ self._by_name = {} # dict[str, ConversionEntry]
13
+
14
+ def add_edge(self, entry):
15
+ """Add a ConversionEntry as a directed edge.
16
+
17
+ If an entry with the same name already exists it is replaced.
18
+ """
19
+ if entry.name in self._by_name:
20
+ self.remove_edge(entry.name)
21
+ self._adjacency.setdefault(entry.source, []).append(entry)
22
+ self._by_name[entry.name] = entry
23
+
24
+ def remove_edge(self, name):
25
+ """Remove an edge by its ConversionEntry.name.
26
+
27
+ Raises ``KeyError`` if the name is not found.
28
+ """
29
+ if name not in self._by_name:
30
+ raise KeyError(f"No conversion named '{name}'")
31
+ entry = self._by_name.pop(name)
32
+ edges = self._adjacency.get(entry.source, [])
33
+ self._adjacency[entry.source] = [e for e in edges if e.name != name]
34
+ if not self._adjacency[entry.source]:
35
+ del self._adjacency[entry.source]
36
+
37
+ def neighbors(self, source):
38
+ """Return all ConversionEntry objects from a given source attribute."""
39
+ return list(self._adjacency.get(source, []))
40
+
41
+ def edges(self):
42
+ """Return all ConversionEntry objects in the graph."""
43
+ return list(self._by_name.values())
44
+
45
+ def has_edge(self, source, target):
46
+ """Check if any edge exists from source to target."""
47
+ return any(e.target == target
48
+ for e in self._adjacency.get(source, []))
49
+
50
+ def find_edges(self, source, target):
51
+ """Return all edges from source to target."""
52
+ return [e for e in self._adjacency.get(source, [])
53
+ if e.target == target]
54
+
55
+ def clear(self):
56
+ """Remove all edges."""
57
+ self._adjacency.clear()
58
+ self._by_name.clear()
@@ -0,0 +1,14 @@
1
+ """Domain-specific conversion modules.
2
+
3
+ Each sub-package registers its own set of ConversionEntry objects.
4
+ To discover the installed modules, call :func:`list_modules`.
5
+ """
6
+
7
+ import importlib
8
+ import pkgutil
9
+
10
+
11
+ def list_modules():
12
+ """Return the names of all installed sub-packages under ``modules/``."""
13
+ return [name for _, name, is_pkg in pkgutil.iter_modules(__path__)
14
+ if is_pkg]
@@ -0,0 +1 @@
1
+ /output/
@@ -0,0 +1,110 @@
1
+ """Foaming filament conversion registrations.
2
+
3
+ Each entry wraps empirical lookup-table data measured for ColorFabb
4
+ VarioShore TPU and ColorFabb LW-PLA foaming filaments.
5
+
6
+ Usage::
7
+
8
+ import pyvcad_attribute_resolver as resolver
9
+ resolver.register_foaming_conversions() # all 5
10
+ resolver.register_tpu_conversions() # TPU-only (3)
11
+ resolver.register_pla_conversions() # PLA-only (2)
12
+ """
13
+
14
+ import pyvcad as pv
15
+ from pyvcad_attribute_resolver.registry import register_conversion
16
+ from .data import (
17
+ TPU_SHORE_HARDNESS,
18
+ TPU_DENSITY,
19
+ TPU_FLOW_COMPENSATION,
20
+ PLA_DENSITY,
21
+ PLA_FLOW_COMPENSATION,
22
+ )
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Helpers
26
+ # ---------------------------------------------------------------------------
27
+
28
+ def _lut_factory(source_attr, target_attr, data):
29
+ """Return a zero-arg callable that builds a fresh LookupTableConverter."""
30
+ entries = [pv.LookupTableEntry(kv[0], kv[0], kv[1]) for kv in data]
31
+
32
+ def _factory():
33
+ return pv.LookupTableConverter(
34
+ [source_attr], [target_attr],
35
+ entries, pv.InterpolationMode.LINEAR,
36
+ )
37
+ return _factory
38
+
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # Public registration functions
42
+ # ---------------------------------------------------------------------------
43
+
44
+ def register_tpu_conversions():
45
+ """Register the three VarioShore TPU conversions."""
46
+ register_conversion(
47
+ source="shore_hardness", target="temperature",
48
+ converter_factory=_lut_factory(
49
+ pv.DefaultAttributes.SHORE_HARDNESS,
50
+ pv.DefaultAttributes.TEMPERATURE,
51
+ TPU_SHORE_HARDNESS,
52
+ ),
53
+ name="tpu_shore_hardness_to_temperature",
54
+ priority=0,
55
+ tags=["foaming_tpu"],
56
+ )
57
+ register_conversion(
58
+ source="density", target="temperature",
59
+ converter_factory=_lut_factory(
60
+ pv.DefaultAttributes.DENSITY,
61
+ pv.DefaultAttributes.TEMPERATURE,
62
+ TPU_DENSITY,
63
+ ),
64
+ name="tpu_density_to_temperature",
65
+ priority=0,
66
+ tags=["foaming_tpu"],
67
+ )
68
+ register_conversion(
69
+ source="temperature", target="flow_rate",
70
+ converter_factory=_lut_factory(
71
+ pv.DefaultAttributes.TEMPERATURE,
72
+ pv.DefaultAttributes.FLOW_RATE,
73
+ TPU_FLOW_COMPENSATION,
74
+ ),
75
+ name="tpu_temperature_to_flow_rate",
76
+ priority=0,
77
+ tags=["foaming_tpu"],
78
+ )
79
+
80
+
81
+ def register_pla_conversions():
82
+ """Register the two LW-PLA conversions."""
83
+ register_conversion(
84
+ source="density", target="temperature",
85
+ converter_factory=_lut_factory(
86
+ pv.DefaultAttributes.DENSITY,
87
+ pv.DefaultAttributes.TEMPERATURE,
88
+ PLA_DENSITY,
89
+ ),
90
+ name="pla_density_to_temperature",
91
+ priority=0,
92
+ tags=["foaming_pla"],
93
+ )
94
+ register_conversion(
95
+ source="temperature", target="flow_rate",
96
+ converter_factory=_lut_factory(
97
+ pv.DefaultAttributes.TEMPERATURE,
98
+ pv.DefaultAttributes.FLOW_RATE,
99
+ PLA_FLOW_COMPENSATION,
100
+ ),
101
+ name="pla_temperature_to_flow_rate",
102
+ priority=0,
103
+ tags=["foaming_pla"],
104
+ )
105
+
106
+
107
+ def register_foaming_conversions():
108
+ """Register all five foaming filament conversions (TPU + PLA)."""
109
+ register_tpu_conversions()
110
+ register_pla_conversions()
@@ -0,0 +1,256 @@
1
+ """Raw empirical lookup-table data for foaming filaments.
2
+
3
+ The raw constants in this module are the maintained source of truth for the
4
+ foaming filament characterization data. Runtime lookup tables are derived from
5
+ the raw measurements so the conversion data and plot data stay synchronized.
6
+ """
7
+
8
+ DESIGNED_WALL_THICKNESS_MM = 0.45
9
+
10
+ # ---------------------------------------------------------------------------
11
+ # ColorFabb VarioShore TPU
12
+ # ---------------------------------------------------------------------------
13
+
14
+ TPU_BASE_FLOW_MULTIPLIER = 1.4
15
+ TPU_DENSITY_SAMPLE_VOLUME_CM3 = 6.3
16
+ TPU_FLOW_RUNTIME_TEMPERATURE_RANGE = (204, 240)
17
+
18
+ #: Temperature (C) -> uncompensated wall-thickness measurements (mm)
19
+ TPU_WALL_EXPANSION_RAW = {
20
+ 196: [0.41, 0.41, 0.40, 0.40],
21
+ 200: [0.43, 0.42, 0.42, 0.42],
22
+ 204: [0.41, 0.41, 0.41, 0.40],
23
+ 208: [0.43, 0.43, 0.43, 0.43],
24
+ 212: [0.48, 0.49, 0.48, 0.47],
25
+ 216: [0.55, 0.55, 0.56, 0.56],
26
+ 220: [0.67, 0.67, 0.65, 0.65],
27
+ 224: [0.74, 0.74, 0.73, 0.73],
28
+ 228: [0.84, 0.83, 0.84, 0.84],
29
+ 232: [0.87, 0.89, 0.90, 0.89],
30
+ 236: [0.91, 0.96, 0.93, 0.92],
31
+ 240: [0.95, 0.95, 0.95, 0.94],
32
+ 244: [0.94, 0.94, 0.95, 0.97],
33
+ 248: [0.97, 0.97, 0.97, 0.95],
34
+ 252: [0.99, 1.01, 0.95, 0.91],
35
+ 256: [0.99, 0.99, 0.95, 0.95],
36
+ 260: [0.99, 0.99, 0.95, 0.96],
37
+ }
38
+
39
+ #: Temperature (C) -> flow factor used during compensated validation
40
+ TPU_WALL_CALIBRATION_FLOW_FACTORS = {
41
+ 204: 1.5460122699386503,
42
+ 208: 1.4651162790697674,
43
+ 212: 1.3125,
44
+ 216: 1.1351351351351349,
45
+ 220: 0.9545454545454544,
46
+ 224: 0.8571428571428571,
47
+ 228: 0.7522388059701494,
48
+ 232: 0.7098591549295774,
49
+ 236: 0.6774193548387096,
50
+ 240: 0.6649076517150396,
51
+ }
52
+
53
+ #: Temperature (C) -> compensated wall-thickness measurements (mm)
54
+ TPU_WALL_CALIBRATION_VALIDATION_RAW = {
55
+ 204: [0.46, 0.47, 0.43, 0.42],
56
+ 208: [0.43, 0.44, 0.41, 0.41],
57
+ 212: [0.46, 0.45, 0.46, 0.47],
58
+ 216: [0.49, 0.44, 0.47, 0.47],
59
+ 220: [0.47, 0.46, 0.47, 0.48],
60
+ 224: [0.47, 0.45, 0.45, 0.49],
61
+ 228: [0.45, 0.43, 0.45, 0.46],
62
+ 232: [0.42, 0.43, 0.44, 0.46],
63
+ 236: [0.44, 0.42, 0.44, 0.43],
64
+ 240: [0.42, 0.43, 0.45, 0.44],
65
+ }
66
+
67
+ #: Temperature (C) -> density block samples with weight and Shore A repeats
68
+ TPU_SHORE_DENSITY_RAW = {
69
+ 196: {"weight_g": 7.1, "shore_hardness": [91.0, 93.5]},
70
+ 200: {"weight_g": 7.1, "shore_hardness": [91.0, 93.5]},
71
+ 204: {"weight_g": 6.0, "shore_hardness": [90.0, 89.0]},
72
+ 208: {"weight_g": 5.6, "shore_hardness": [85.5, 87.5]},
73
+ 212: {"weight_g": 5.3, "shore_hardness": [83.0, 86.0]},
74
+ 216: {"weight_g": 4.4, "shore_hardness": [76.5, 79.0]},
75
+ 220: {"weight_g": 3.5, "shore_hardness": [71.0, 74.0]},
76
+ 224: {"weight_g": 3.1, "shore_hardness": [68.5, 69.0]},
77
+ 228: {"weight_g": 2.8, "shore_hardness": [65.0, 66.0]},
78
+ 232: {"weight_g": 2.6, "shore_hardness": [62.0, 61.0]},
79
+ }
80
+
81
+ # Backward-compatible private name used by older local plotting code.
82
+ _TPU_SHORE_HARDNESS_RAW = {
83
+ temperature: row["shore_hardness"]
84
+ for temperature, row in TPU_SHORE_DENSITY_RAW.items()
85
+ }
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # ColorFabb LW-PLA
89
+ # ---------------------------------------------------------------------------
90
+
91
+ PLA_BASE_FLOW_MULTIPLIER = 1.25
92
+ PLA_DENSITY_SAMPLE_VOLUME_CM3 = 3.7
93
+ PLA_FLOW_RUNTIME_TEMPERATURE_RANGE = (220, 256)
94
+
95
+ #: Temperature (C) -> uncompensated wall-thickness measurements (mm)
96
+ PLA_WALL_EXPANSION_RAW = {
97
+ 196: [0.40, 0.40, 0.40, 0.39],
98
+ 200: [0.39, 0.39, 0.40, 0.40],
99
+ 204: [0.39, 0.40, 0.40, 0.40],
100
+ 208: [0.40, 0.41, 0.39, 0.39],
101
+ 212: [0.39, 0.39, 0.41, 0.41],
102
+ 216: [0.39, 0.39, 0.40, 0.39],
103
+ 220: [0.39, 0.40, 0.40, 0.40],
104
+ 224: [0.45, 0.45, 0.46, 0.47],
105
+ 228: [0.52, 0.52, 0.52, 0.51],
106
+ 232: [0.58, 0.57, 0.58, 0.57],
107
+ 236: [0.66, 0.65, 0.65, 0.65],
108
+ 240: [0.74, 0.73, 0.74, 0.74],
109
+ 244: [0.76, 0.80, 0.79, 0.78],
110
+ 248: [0.82, 0.82, 0.81, 0.80],
111
+ 252: [0.84, 0.83, 0.82, 0.84],
112
+ 256: [0.86, 0.86, 0.84, 0.85],
113
+ 258: [0.85, 0.84, 0.86, 0.86],
114
+ 260: [0.85, 0.83, 0.84, 0.83],
115
+ 261: [0.83, 0.82, 0.82, 0.81],
116
+ 262: [0.79, 0.82, 0.80, 0.80],
117
+ 263: [0.80, 0.80, 0.78, 0.79],
118
+ 264: [0.79, 0.78, 0.78, 0.76],
119
+ 268: [0.80, 0.77, 0.74, 0.79],
120
+ 272: [0.77, 0.76, 0.76, 0.77],
121
+ 276: [0.77, 0.74, 0.74, 0.75],
122
+ 280: [0.71, 0.73, 0.74, 0.70],
123
+ }
124
+
125
+ #: Temperature (C) -> flow factor used during compensated validation
126
+ PLA_WALL_CALIBRATION_FLOW_FACTORS = {
127
+ 220: 1.4150943396226419,
128
+ 224: 1.2295081967213115,
129
+ 228: 1.0869565217391304,
130
+ 232: 0.9782608695652174,
131
+ 236: 0.8620689655172414,
132
+ 240: 0.7627118644067796,
133
+ 244: 0.7188498402555911,
134
+ 248: 0.6923076923076923,
135
+ 252: 0.6756756756756757,
136
+ 256: 0.6598240469208211,
137
+ }
138
+
139
+ #: Temperature (C) -> compensated wall-thickness measurements (mm)
140
+ PLA_WALL_CALIBRATION_VALIDATION_RAW = {
141
+ 220: [0.45, 0.46, 0.46, 0.45],
142
+ 224: [0.44, 0.44, 0.44, 0.45],
143
+ 228: [0.47, 0.46, 0.47, 0.47],
144
+ 232: [0.49, 0.48, 0.47, 0.47],
145
+ 236: [0.49, 0.48, 0.50, 0.47],
146
+ 240: [0.48, 0.48, 0.47, 0.49],
147
+ 244: [0.48, 0.47, 0.48, 0.49],
148
+ 248: [0.48, 0.45, 0.46, 0.46],
149
+ 252: [0.43, 0.44, 0.46, 0.45],
150
+ 256: [0.45, 0.45, 0.44, 0.45],
151
+ }
152
+
153
+ #: Temperature (C) -> density block samples with flow factor and weight
154
+ PLA_DENSITY_RAW = {
155
+ 216: {"flow_factor": 1.43312101910828, "weight_g": 3.7},
156
+ 220: {"flow_factor": 1.4150943396226419, "weight_g": 3.7},
157
+ 224: {"flow_factor": 1.2295081967213115, "weight_g": 3.2},
158
+ 228: {"flow_factor": 1.0869565217391304, "weight_g": 2.9},
159
+ 232: {"flow_factor": 0.9782608695652174, "weight_g": 2.6},
160
+ 236: {"flow_factor": 0.8620689655172414, "weight_g": 2.2},
161
+ 240: {"flow_factor": 0.7627118644067796, "weight_g": 2.0},
162
+ 244: {"flow_factor": 0.7188498402555911, "weight_g": 1.9},
163
+ 248: {"flow_factor": 0.6923076923076923, "weight_g": 1.9},
164
+ 252: {"flow_factor": 0.6756756756756757, "weight_g": 1.8},
165
+ 256: {"flow_factor": 0.6598240469208211, "weight_g": 1.8},
166
+ 260: {"flow_factor": 0.671641791044776, "weight_g": 1.8},
167
+ 264: {"flow_factor": 0.7234726688102894, "weight_g": 1.8},
168
+ }
169
+
170
+
171
+ def _mean(values):
172
+ return sum(values) / len(values)
173
+
174
+
175
+ def _within_range(value, bounds):
176
+ return bounds[0] <= value <= bounds[1]
177
+
178
+
179
+ def _flow_compensation_from_wall_expansion(raw_data, base_multiplier, bounds):
180
+ rows = []
181
+ for temperature, samples in raw_data.items():
182
+ if _within_range(temperature, bounds):
183
+ flow_rate = (
184
+ base_multiplier
185
+ * DESIGNED_WALL_THICKNESS_MM
186
+ / _mean(samples)
187
+ )
188
+ rows.append((temperature, flow_rate))
189
+ return sorted(rows)
190
+
191
+
192
+ def _unique_density_to_temperature(rows):
193
+ seen = set()
194
+ unique_rows = []
195
+ for density, temperature in sorted(rows, key=lambda row: row[1]):
196
+ if density in seen:
197
+ continue
198
+ seen.add(density)
199
+ unique_rows.append((density, temperature))
200
+ return sorted(unique_rows)
201
+
202
+
203
+ def _unique_input_rows(rows):
204
+ seen = set()
205
+ unique_rows = []
206
+ for input_value, output_value in sorted(rows, key=lambda row: row[1]):
207
+ if input_value in seen:
208
+ continue
209
+ seen.add(input_value)
210
+ unique_rows.append((input_value, output_value))
211
+ return sorted(unique_rows)
212
+
213
+
214
+ def _tpu_density_rows():
215
+ rows = []
216
+ for temperature, row in TPU_SHORE_DENSITY_RAW.items():
217
+ density = row["weight_g"] / TPU_DENSITY_SAMPLE_VOLUME_CM3
218
+ rows.append((density, temperature))
219
+ return rows
220
+
221
+
222
+ def _pla_density_rows():
223
+ rows = []
224
+ for temperature, row in PLA_DENSITY_RAW.items():
225
+ density = row["weight_g"] / PLA_DENSITY_SAMPLE_VOLUME_CM3
226
+ rows.append((density, temperature))
227
+ return rows
228
+
229
+
230
+ #: Shore hardness (Shore A) -> temperature (C)
231
+ TPU_SHORE_HARDNESS = _unique_input_rows(
232
+ [
233
+ (_mean(row["shore_hardness"]), temperature)
234
+ for temperature, row in TPU_SHORE_DENSITY_RAW.items()
235
+ ]
236
+ )
237
+
238
+ #: Density (g/cm^3) -> temperature (C)
239
+ TPU_DENSITY = _unique_density_to_temperature(_tpu_density_rows())
240
+
241
+ #: Temperature (C) -> flow rate multiplier
242
+ TPU_FLOW_COMPENSATION = _flow_compensation_from_wall_expansion(
243
+ TPU_WALL_EXPANSION_RAW,
244
+ TPU_BASE_FLOW_MULTIPLIER,
245
+ TPU_FLOW_RUNTIME_TEMPERATURE_RANGE,
246
+ )
247
+
248
+ #: Density (g/cm^3) -> temperature (C)
249
+ PLA_DENSITY = _unique_density_to_temperature(_pla_density_rows())
250
+
251
+ #: Temperature (C) -> flow rate multiplier
252
+ PLA_FLOW_COMPENSATION = _flow_compensation_from_wall_expansion(
253
+ PLA_WALL_EXPANSION_RAW,
254
+ PLA_BASE_FLOW_MULTIPLIER,
255
+ PLA_FLOW_RUNTIME_TEMPERATURE_RANGE,
256
+ )