Files
create/src/Mod/CAM/Path/Tool/machine/models/machine.py
2025-05-19 20:27:28 +02:00

435 lines
18 KiB
Python

# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2025 Samuel Abels <knipknap@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
import uuid
import json
import FreeCAD
from FreeCAD import Base
from typing import Optional, Union, Mapping, List
from ...assets import Asset, AssetUri, AssetSerializer
class Machine(Asset):
"""Represents a machine with various operational parameters."""
asset_type: str = "machine"
API_VERSION = 1
UNIT_CONVERSIONS = {
"hp": 745.7, # hp to W
"in-lbf": 0.112985, # in-lbf to N*m
"inch/min": 25.4, # inch/min to mm/min
"rpm": 1.0 / 60.0, # rpm to 1/s
"kW": 1000.0, # kW to W
"Nm": 1.0, # Nm to N*m
"mm/min": 1.0, # mm/min to mm/min
}
def __init__(
self,
label: str = "Machine",
max_power: Union[int, float, FreeCAD.Units.Quantity] = 2,
min_rpm: Union[int, float, FreeCAD.Units.Quantity] = 3000,
max_rpm: Union[int, float, FreeCAD.Units.Quantity] = 60000,
max_torque: Optional[Union[int, float, FreeCAD.Units.Quantity]] = None,
peak_torque_rpm: Optional[Union[int, float, FreeCAD.Units.Quantity]] = None,
min_feed: Union[int, float, FreeCAD.Units.Quantity] = 1,
max_feed: Union[int, float, FreeCAD.Units.Quantity] = 2000,
id: Optional[str] = None,
) -> None:
"""
Initializes a Machine object.
Args:
label: The label of the machine.
max_power: The maximum power of the machine (kW or Quantity).
min_rpm: The minimum RPM of the machine (RPM or Quantity).
max_rpm: The maximum RPM of the machine (RPM or Quantity).
max_torque: The maximum torque of the machine (Nm or Quantity).
peak_torque_rpm: The RPM at which peak torque is achieved
(RPM or Quantity).
min_feed: The minimum feed rate of the machine
(mm/min or Quantity).
max_feed: The maximum feed rate of the machine
(mm/min or Quantity).
id: The unique identifier of the machine.
"""
self.id = id or str(uuid.uuid1())
self._label = label
# Initialize max_power (W)
if isinstance(max_power, FreeCAD.Units.Quantity):
self._max_power = max_power.getValueAs("W").Value
elif isinstance(max_power, (int, float)):
self._max_power = max_power * self.UNIT_CONVERSIONS["kW"]
else:
self._max_power = 2000.0
# Initialize min_rpm (1/s)
if isinstance(min_rpm, FreeCAD.Units.Quantity):
try:
self._min_rpm = min_rpm.getValueAs("1/s").Value
except (Base.ParserError, ValueError):
self._min_rpm = min_rpm.Value * self.UNIT_CONVERSIONS["rpm"]
elif isinstance(min_rpm, (int, float)):
self._min_rpm = min_rpm * self.UNIT_CONVERSIONS["rpm"]
else:
self._min_rpm = 3000 * self.UNIT_CONVERSIONS["rpm"]
# Initialize max_rpm (1/s)
if isinstance(max_rpm, FreeCAD.Units.Quantity):
try:
self._max_rpm = max_rpm.getValueAs("1/s").Value
except (Base.ParserError, ValueError):
self._max_rpm = max_rpm.Value * self.UNIT_CONVERSIONS["rpm"]
elif isinstance(max_rpm, (int, float)):
self._max_rpm = max_rpm * self.UNIT_CONVERSIONS["rpm"]
else:
self._max_rpm = 60000 * self.UNIT_CONVERSIONS["rpm"]
# Initialize min_feed (mm/min)
if isinstance(min_feed, FreeCAD.Units.Quantity):
self._min_feed = min_feed.getValueAs("mm/min").Value
elif isinstance(min_feed, (int, float)):
self._min_feed = min_feed
else:
self._min_feed = 1.0
# Initialize max_feed (mm/min)
if isinstance(max_feed, FreeCAD.Units.Quantity):
self._max_feed = max_feed.getValueAs("mm/min").Value
elif isinstance(max_feed, (int, float)):
self._max_feed = max_feed
else:
self._max_feed = 2000.0
# Initialize peak_torque_rpm (1/s)
if isinstance(peak_torque_rpm, FreeCAD.Units.Quantity):
try:
self._peak_torque_rpm = peak_torque_rpm.getValueAs("1/s").Value
except (Base.ParserError, ValueError):
self._peak_torque_rpm = peak_torque_rpm.Value * self.UNIT_CONVERSIONS["rpm"]
elif isinstance(peak_torque_rpm, (int, float)):
self._peak_torque_rpm = peak_torque_rpm * self.UNIT_CONVERSIONS["rpm"]
else:
self._peak_torque_rpm = self._max_rpm / 3
# Initialize max_torque (N*m)
if isinstance(max_torque, FreeCAD.Units.Quantity):
self._max_torque = max_torque.getValueAs("Nm").Value
elif isinstance(max_torque, (int, float)):
self._max_torque = max_torque
else:
# Convert 1/s to rpm
peak_rpm_for_calc = self._peak_torque_rpm * 60
self._max_torque = (
self._max_power * 9.5488 / peak_rpm_for_calc if peak_rpm_for_calc else float("inf")
)
def get_id(self) -> str:
"""Returns the unique identifier for the Machine instance."""
return self.id
def to_dict(self) -> dict:
"""Returns a dictionary representation of the Machine."""
return {
"version": self.API_VERSION,
"id": self.id,
"label": self.label,
"max_power": self._max_power, # W
"min_rpm": self._min_rpm, # 1/s
"max_rpm": self._max_rpm, # 1/s
"max_torque": self._max_torque, # Nm
"peak_torque_rpm": self._peak_torque_rpm, # 1/s
"min_feed": self._min_feed, # mm/min
"max_feed": self._max_feed, # mm/min
}
def to_bytes(self, serializer: AssetSerializer) -> bytes:
"""Serializes the Machine object to bytes using to_dict."""
data_dict = self.to_dict()
json_str = json.dumps(data_dict)
return json_str.encode("utf-8")
@classmethod
def from_dict(cls, data_dict: dict, id: str) -> "Machine":
"""Creates a Machine instance from a dictionary."""
machine = cls(
label=data_dict.get("label", "Machine"),
max_power=data_dict.get("max_power", 2000.0), # W
min_rpm=data_dict.get("min_rpm", 3000 * cls.UNIT_CONVERSIONS["rpm"]), # 1/s
max_rpm=data_dict.get("max_rpm", 60000 * cls.UNIT_CONVERSIONS["rpm"]), # 1/s
max_torque=data_dict.get("max_torque", None), # Nm
peak_torque_rpm=data_dict.get("peak_torque_rpm", None), # 1/s
min_feed=data_dict.get("min_feed", 1.0), # mm/min
max_feed=data_dict.get("max_feed", 2000.0), # mm/min
id=id,
)
return machine
@classmethod
def from_bytes(
cls,
data: bytes,
id: str,
dependencies: Optional[Mapping[AssetUri, Asset]],
) -> "Machine":
"""
Deserializes bytes into a Machine instance using from_dict.
"""
# If dependencies is None, it's fine as Machine doesn't use it.
data_dict = json.loads(data.decode("utf-8"))
return cls.from_dict(data_dict, id)
@classmethod
def dependencies(cls, data: bytes) -> List[AssetUri]:
"""Returns a list of AssetUri dependencies parsed from the serialized data."""
return [] # Machine has no dependencies
@property
def max_power(self) -> FreeCAD.Units.Quantity:
return FreeCAD.Units.Quantity(self._max_power, "W")
@property
def min_rpm(self) -> FreeCAD.Units.Quantity:
return FreeCAD.Units.Quantity(self._min_rpm, "1/s")
@property
def max_rpm(self) -> FreeCAD.Units.Quantity:
return FreeCAD.Units.Quantity(self._max_rpm, "1/s")
@property
def max_torque(self) -> FreeCAD.Units.Quantity:
return FreeCAD.Units.Quantity(self._max_torque, "Nm")
@property
def peak_torque_rpm(self) -> FreeCAD.Units.Quantity:
return FreeCAD.Units.Quantity(self._peak_torque_rpm, "1/s")
@property
def min_feed(self) -> FreeCAD.Units.Quantity:
return FreeCAD.Units.Quantity(self._min_feed, "mm/min")
@property
def max_feed(self) -> FreeCAD.Units.Quantity:
return FreeCAD.Units.Quantity(self._max_feed, "mm/min")
@property
def label(self) -> str:
return self._label
@label.setter
def label(self, label: str) -> None:
self._label = label
def get_min_rpm_value(self) -> float:
"""Helper method to get minimum RPM value for display/testing."""
return self._min_rpm * 60
def get_max_rpm_value(self) -> float:
"""Helper method to get maximum RPM value for display/testing."""
return self._max_rpm * 60
def get_peak_torque_rpm_value(self) -> float:
"""Helper method to get peak torque RPM value for display/testing."""
return self._peak_torque_rpm * 60
def validate(self) -> None:
"""Validates the machine parameters."""
if not self.label:
raise AttributeError("Machine name is required")
if self._peak_torque_rpm > self._max_rpm:
err = ("Peak Torque RPM {ptrpm:.2f} must be less than max RPM " "{max_rpm:.2f}").format(
ptrpm=self._peak_torque_rpm * 60, max_rpm=self._max_rpm * 60
)
raise AttributeError(err)
if self._max_rpm <= self._min_rpm:
raise AttributeError("Max RPM must be larger than min RPM")
if self._max_feed <= self._min_feed:
raise AttributeError("Max feed must be larger than min feed")
def get_torque_at_rpm(self, rpm: Union[int, float, FreeCAD.Units.Quantity]) -> float:
"""
Calculates the torque at a given RPM.
Args:
rpm: The RPM value (int, float, or Quantity).
Returns:
The torque at the given RPM in Nm.
"""
if isinstance(rpm, FreeCAD.Units.Quantity):
try:
rpm_hz = rpm.getValueAs("1/s").Value
except (Base.ParserError, ValueError):
rpm_hz = rpm.Value * self.UNIT_CONVERSIONS["rpm"]
else:
rpm_hz = rpm * self.UNIT_CONVERSIONS["rpm"]
max_torque_nm = self._max_torque
peak_torque_rpm_hz = self._peak_torque_rpm
peak_rpm_for_calc = peak_torque_rpm_hz * 60
rpm_for_calc = rpm_hz * 60
torque_at_current_rpm = (
self._max_power * 9.5488 / rpm_for_calc if rpm_for_calc else float("inf")
)
if rpm_for_calc <= peak_rpm_for_calc:
torque_at_current_rpm = (
max_torque_nm / peak_rpm_for_calc * rpm_for_calc
if peak_rpm_for_calc
else float("inf")
)
return min(max_torque_nm, torque_at_current_rpm)
def set_max_power(self, power: Union[int, float], unit: Optional[str] = None) -> None:
"""Sets the maximum power of the machine."""
unit = unit or "kW"
if unit in self.UNIT_CONVERSIONS:
power_value = power * self.UNIT_CONVERSIONS[unit]
else:
power_value = FreeCAD.Units.Quantity(power, unit).getValueAs("W").Value
self._max_power = power_value
if self._max_power <= 0:
raise AttributeError("Max power must be positive")
def set_min_rpm(self, min_rpm: Union[int, float, FreeCAD.Units.Quantity]) -> None:
"""Sets the minimum RPM of the machine."""
if isinstance(min_rpm, FreeCAD.Units.Quantity):
try:
min_rpm_value = min_rpm.getValueAs("1/s").Value
except (Base.ParserError, ValueError):
min_rpm_value = min_rpm.Value * self.UNIT_CONVERSIONS["rpm"]
else:
min_rpm_value = min_rpm * self.UNIT_CONVERSIONS["rpm"]
self._min_rpm = min_rpm_value
if self._min_rpm < 0:
raise AttributeError("Min RPM cannot be negative")
if self._min_rpm >= self._max_rpm:
self._max_rpm = min_rpm_value + 1.0 / 60.0
def set_max_rpm(self, max_rpm: Union[int, float, FreeCAD.Units.Quantity]) -> None:
"""Sets the maximum RPM of the machine."""
if isinstance(max_rpm, FreeCAD.Units.Quantity):
try:
max_rpm_value = max_rpm.getValueAs("1/s").Value
except (Base.ParserError, ValueError):
max_rpm_value = max_rpm.Value * self.UNIT_CONVERSIONS["rpm"]
else:
max_rpm_value = max_rpm * self.UNIT_CONVERSIONS["rpm"]
self._max_rpm = max_rpm_value
if self._max_rpm <= 0:
raise AttributeError("Max RPM must be positive")
if self._max_rpm <= self._min_rpm:
self._min_rpm = max(0, max_rpm_value - 1.0 / 60.0)
def set_min_feed(
self,
min_feed: Union[int, float, FreeCAD.Units.Quantity],
unit: Optional[str] = None,
) -> None:
"""Sets the minimum feed rate of the machine."""
unit = unit or "mm/min"
if unit in self.UNIT_CONVERSIONS:
min_feed_value = min_feed * self.UNIT_CONVERSIONS[unit]
else:
min_feed_value = FreeCAD.Units.Quantity(min_feed, unit).getValueAs("mm/min").Value
self._min_feed = min_feed_value
if self._min_feed < 0:
raise AttributeError("Min feed cannot be negative")
if self._min_feed >= self._max_feed:
self._max_feed = min_feed_value + 1.0
def set_max_feed(
self,
max_feed: Union[int, float, FreeCAD.Units.Quantity],
unit: Optional[str] = None,
) -> None:
"""Sets the maximum feed rate of the machine."""
unit = unit or "mm/min"
if unit in self.UNIT_CONVERSIONS:
max_feed_value = max_feed * self.UNIT_CONVERSIONS[unit]
else:
max_feed_value = FreeCAD.Units.Quantity(max_feed, unit).getValueAs("mm/min").Value
self._max_feed = max_feed_value
if self._max_feed <= 0:
raise AttributeError("Max feed must be positive")
if self._max_feed <= self._min_feed:
self._min_feed = max(0, max_feed_value - 1.0)
def set_peak_torque_rpm(
self, peak_torque_rpm: Union[int, float, FreeCAD.Units.Quantity]
) -> None:
"""Sets the peak torque RPM of the machine."""
if isinstance(peak_torque_rpm, FreeCAD.Units.Quantity):
try:
peak_torque_rpm_value = peak_torque_rpm.getValueAs("1/s").Value
except (Base.ParserError, ValueError):
peak_torque_rpm_value = peak_torque_rpm.Value * self.UNIT_CONVERSIONS["rpm"]
else:
peak_torque_rpm_value = peak_torque_rpm * self.UNIT_CONVERSIONS["rpm"]
self._peak_torque_rpm = peak_torque_rpm_value
if self._peak_torque_rpm < 0:
raise AttributeError("Peak torque RPM cannot be negative")
def set_max_torque(
self,
max_torque: Union[int, float, FreeCAD.Units.Quantity],
unit: Optional[str] = None,
) -> None:
"""Sets the maximum torque of the machine."""
unit = unit or "Nm"
if unit in self.UNIT_CONVERSIONS:
max_torque_value = max_torque * self.UNIT_CONVERSIONS[unit]
else:
max_torque_value = FreeCAD.Units.Quantity(max_torque, unit).getValueAs("Nm").Value
self._max_torque = max_torque_value
if self._max_torque <= 0:
raise AttributeError("Max torque must be positive")
def dump(self, do_print: bool = True) -> Optional[str]:
"""
Dumps machine information to console or returns it as a string.
Args:
do_print: If True, prints the information to the console.
If False, returns the information as a string.
Returns:
A formatted string containing machine information if do_print is
False, otherwise None.
"""
min_rpm_value = self._min_rpm * 60
max_rpm_value = self._max_rpm * 60
peak_torque_rpm_value = self._peak_torque_rpm * 60
output = ""
output += f"Machine {self.label}:\n"
output += f" Max power: {self._max_power:.2f} W\n"
output += f" RPM: {min_rpm_value:.2f} RPM - {max_rpm_value:.2f} RPM\n"
output += f" Feed: {self.min_feed.UserString} - " f"{self.max_feed.UserString}\n"
output += (
f" Peak torque: {self._max_torque:.2f} Nm at " f"{peak_torque_rpm_value:.2f} RPM\n"
)
output += f" Max_torque: {self._max_torque} Nm\n"
if do_print:
print(output)
return output