ifemm 0.2.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.
- ifemm/__init__.py +10 -0
- ifemm/constants.py +62 -0
- ifemm/core/__init__.py +2 -0
- ifemm/core/deserialization.py +92 -0
- ifemm/core/states.py +19 -0
- ifemm/core/syntax.py +181 -0
- ifemm/interpreter/__init__.py +2 -0
- ifemm/interpreter/attributes.py +33 -0
- ifemm/interpreter/magnetic/__init__.py +2 -0
- ifemm/interpreter/magnetic/definitions.py +127 -0
- ifemm/interpreter/magnetic/schema.py +180 -0
- ifemm/parser.py +126 -0
- ifemm/utilities/__init__.py +2 -0
- ifemm/utilities/errors.py +62 -0
- ifemm-0.2.0.dist-info/METADATA +120 -0
- ifemm-0.2.0.dist-info/RECORD +18 -0
- ifemm-0.2.0.dist-info/WHEEL +4 -0
- ifemm-0.2.0.dist-info/licenses/LICENSE +21 -0
ifemm/__init__.py
ADDED
ifemm/constants.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filename: constants.py
|
|
3
|
+
|
|
4
|
+
Description:
|
|
5
|
+
This file contains constants regarding
|
|
6
|
+
to the parsing of .ans files.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# Validate file types for the parser
|
|
10
|
+
FILE_TYPES = [
|
|
11
|
+
".ans"
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
# Validate File Formats
|
|
15
|
+
FILE_FORMATS = [
|
|
16
|
+
4.0
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
# Validate Problem Parameters
|
|
20
|
+
PROBLEM_PARAMETERS = [
|
|
21
|
+
"format",
|
|
22
|
+
"frequency",
|
|
23
|
+
"precision",
|
|
24
|
+
"minangle",
|
|
25
|
+
"depth",
|
|
26
|
+
"lengthunits",
|
|
27
|
+
"coordinates",
|
|
28
|
+
"problemtype",
|
|
29
|
+
"comment"
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
# Validate Block Section Names
|
|
33
|
+
BLOCK_SECTIONS = [
|
|
34
|
+
"pointprops",
|
|
35
|
+
"bdryprops",
|
|
36
|
+
"blockprops",
|
|
37
|
+
"circuitprops"
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
# Validate Block Pairs
|
|
41
|
+
BLOCK_PAIRS = {
|
|
42
|
+
"<beginpoint>": "<endpoint>",
|
|
43
|
+
"<beginbdry>": "<endbdry>",
|
|
44
|
+
"<beginblock>": "<endblock>",
|
|
45
|
+
"<begincircuit>": "<endcircuit>"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Validate Data Section Names
|
|
49
|
+
DATA_SECTIONS = [
|
|
50
|
+
"numblocklabels",
|
|
51
|
+
"numarcsegments",
|
|
52
|
+
"conductorprops",
|
|
53
|
+
"numpoints",
|
|
54
|
+
"numsegments",
|
|
55
|
+
"numholes",
|
|
56
|
+
"<bhpoints>",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
# Validate Solution Section Name
|
|
60
|
+
SOLUTION_SECTION = [
|
|
61
|
+
"solution"
|
|
62
|
+
]
|
ifemm/core/__init__.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filename: deserialization.py
|
|
3
|
+
|
|
4
|
+
Description:
|
|
5
|
+
Type conversion for .ans format values.
|
|
6
|
+
|
|
7
|
+
Converts strings to Python primitives:
|
|
8
|
+
(int, float, complex, bool, null/none, str).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from ifemm.utilities.errors import FailedCasting
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Deserialize:
|
|
19
|
+
""" Deserialization from text to primitives """
|
|
20
|
+
@classmethod
|
|
21
|
+
def cast(cls, text: str) -> int | float | complex | bool | None | str:
|
|
22
|
+
""" Converts text to python primitives """
|
|
23
|
+
if not isinstance(text, str):
|
|
24
|
+
err = f"Expected str, got {type(text).__name__}"
|
|
25
|
+
raise FailedCasting(text, err)
|
|
26
|
+
|
|
27
|
+
text = text.strip()
|
|
28
|
+
if cls.is_quoted(text):
|
|
29
|
+
# Handles quoted strings first
|
|
30
|
+
return str(cls.strip_quotes(text))
|
|
31
|
+
|
|
32
|
+
# Try integer value
|
|
33
|
+
try: return int(text)
|
|
34
|
+
except ValueError: pass
|
|
35
|
+
|
|
36
|
+
# Try float
|
|
37
|
+
try: return float(text)
|
|
38
|
+
except ValueError: pass
|
|
39
|
+
|
|
40
|
+
# Try complex
|
|
41
|
+
try: return complex(text)
|
|
42
|
+
except ValueError: pass
|
|
43
|
+
|
|
44
|
+
# Check boolean
|
|
45
|
+
lower = text.lower()
|
|
46
|
+
if lower == "true": return True
|
|
47
|
+
if lower == "false": return False
|
|
48
|
+
|
|
49
|
+
# Check null/None
|
|
50
|
+
if text.lower() in ("null", "none"): return None
|
|
51
|
+
|
|
52
|
+
# Default to string
|
|
53
|
+
return str(text)
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def cast_list(cls, items: list[str]) -> list[Any]:
|
|
57
|
+
""" Casts values within list into primitives """
|
|
58
|
+
casted_list = []
|
|
59
|
+
|
|
60
|
+
for item in items:
|
|
61
|
+
# Casts each item as a python primitive
|
|
62
|
+
casted_list.append(cls.cast(item))
|
|
63
|
+
|
|
64
|
+
return casted_list
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def is_quoted(cls, text: str) -> bool:
|
|
68
|
+
""" Check if text is surrounded by quotes """
|
|
69
|
+
text = text.strip()
|
|
70
|
+
if len(text) < 2:
|
|
71
|
+
# Empty quoted text Ex. ("")
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
if text.startswith('"') and text.endswith('"'):
|
|
75
|
+
# Start and end with double quotes
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
if text.startswith("'") and text.endswith("'"):
|
|
79
|
+
# Start and end with single quotes
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def strip_quotes(cls, text: str) -> str:
|
|
86
|
+
""" Removes surrounding quotes if present """
|
|
87
|
+
text = text.strip()
|
|
88
|
+
if cls.is_quoted(text):
|
|
89
|
+
# Removes double quotation ref. "{Text}"
|
|
90
|
+
return text[1:-1]
|
|
91
|
+
|
|
92
|
+
return text
|
ifemm/core/states.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filename: state.py
|
|
3
|
+
|
|
4
|
+
Description:
|
|
5
|
+
This file contains the parser state
|
|
6
|
+
allowing for access across modules
|
|
7
|
+
without circular implements.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ParserState:
|
|
15
|
+
""" Stores the state of the parser """
|
|
16
|
+
index: int = 0
|
|
17
|
+
section: str | None = None
|
|
18
|
+
block: str | None = None
|
|
19
|
+
content: dict | None = None
|
ifemm/core/syntax.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filename: syntax.py
|
|
3
|
+
|
|
4
|
+
Description:
|
|
5
|
+
Syntaxes for .ans format.
|
|
6
|
+
|
|
7
|
+
Handles block section and data
|
|
8
|
+
section parsing.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from ifemm.core.states import ParserState
|
|
12
|
+
from ifemm.utilities.errors import ParserError
|
|
13
|
+
from ifemm.core.deserialization import Deserialize
|
|
14
|
+
|
|
15
|
+
from ifemm.constants import BLOCK_PAIRS, DATA_SECTIONS
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BlockExtraction:
|
|
19
|
+
""" Extract arbitrary block section data from .ans format """
|
|
20
|
+
@classmethod
|
|
21
|
+
def extract(
|
|
22
|
+
cls, lines: list[str], items: int, state: ParserState
|
|
23
|
+
) -> tuple[dict, ParserState]:
|
|
24
|
+
""" Extracts the block section """
|
|
25
|
+
if items == 0:
|
|
26
|
+
# If the entry has zero data. Returns None
|
|
27
|
+
return {}, state
|
|
28
|
+
|
|
29
|
+
block = {}
|
|
30
|
+
num_item = 0
|
|
31
|
+
|
|
32
|
+
# Initializes the first item
|
|
33
|
+
block[num_item] = {}
|
|
34
|
+
while state.index < len(lines):
|
|
35
|
+
line = lines[state.index].strip()
|
|
36
|
+
state.index += 1
|
|
37
|
+
|
|
38
|
+
if cls._is_new_blocks(line):
|
|
39
|
+
state.block = line.strip().lower()
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
if state.block and cls._is_close_block(line, state):
|
|
43
|
+
state.block = None
|
|
44
|
+
num_item += 1
|
|
45
|
+
|
|
46
|
+
# Returns the result after iteration across items
|
|
47
|
+
if num_item == items: return block, state
|
|
48
|
+
|
|
49
|
+
# Adds the entry for the next item
|
|
50
|
+
block[num_item] = {}
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
if state.block:
|
|
54
|
+
# Extracts block and cases the values within the block
|
|
55
|
+
name, raw_value = cls._extract_block_value(line, state)
|
|
56
|
+
value = Deserialize.cast(raw_value)
|
|
57
|
+
|
|
58
|
+
if name in DATA_SECTIONS:
|
|
59
|
+
# Parses the data section syntaxes
|
|
60
|
+
data, state = DataExtraction.extract(lines, value, state)
|
|
61
|
+
block[num_item][name] = data
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
# Adds the name and value to the block and section
|
|
65
|
+
block[num_item][name] = value
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
msg = f"Failed to parse block section, block: {block}, items: {items}"
|
|
69
|
+
raise ParserError(cls.__name__, msg)
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def _is_new_blocks(cls, line: str) -> bool:
|
|
73
|
+
""" Checks if its a block section """
|
|
74
|
+
if line.lower() in BLOCK_PAIRS:
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def _is_close_block(cls, line: str, state: ParserState) -> bool:
|
|
81
|
+
if state.block is None:
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
if line.lower() in BLOCK_PAIRS[state.block]:
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def _extract_block_value(cls, line: str, state: ParserState) -> tuple[str, str]:
|
|
91
|
+
""" Extract block name and value from line """
|
|
92
|
+
equal_sign = line.find("=")
|
|
93
|
+
if equal_sign == -1:
|
|
94
|
+
msg = f"Malformed block section found in {line!r}, Index: {state.index}"
|
|
95
|
+
raise ParserError(cls.__name__, msg)
|
|
96
|
+
|
|
97
|
+
# Returns stripped lower name and stripped value
|
|
98
|
+
return line[:equal_sign-1].strip().lower(), line[equal_sign+1:].strip()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class DataExtraction:
|
|
102
|
+
""" Extract arbitrary data section from .ans format """
|
|
103
|
+
@classmethod
|
|
104
|
+
def extract(
|
|
105
|
+
cls, lines: list[str], items: int, state: ParserState
|
|
106
|
+
) -> tuple[dict, ParserState]:
|
|
107
|
+
""" Extracts the data section """
|
|
108
|
+
if items == 0:
|
|
109
|
+
# If the entry has zero data. Returns None
|
|
110
|
+
return {}, state
|
|
111
|
+
|
|
112
|
+
data = {}
|
|
113
|
+
num_item = 0
|
|
114
|
+
while state.index < len(lines):
|
|
115
|
+
line = lines[state.index].strip()
|
|
116
|
+
state.index += 1
|
|
117
|
+
|
|
118
|
+
# Splits the line into values
|
|
119
|
+
values = line.split()
|
|
120
|
+
data[num_item] = Deserialize.cast_list(values)
|
|
121
|
+
|
|
122
|
+
num_item += 1
|
|
123
|
+
|
|
124
|
+
# Returns the result after iteration across items
|
|
125
|
+
if num_item == items: return data, state
|
|
126
|
+
|
|
127
|
+
msg = "Failed to parse data section"
|
|
128
|
+
raise ParserError(cls.__name__, msg)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class SolutionExtraction:
|
|
132
|
+
""" Extract arbitrary solution data from .ans format"""
|
|
133
|
+
@classmethod
|
|
134
|
+
def extract(cls, lines: list[str], state: ParserState) -> tuple[dict, ParserState]:
|
|
135
|
+
""" Extracts the solution data """
|
|
136
|
+
data = {}
|
|
137
|
+
rows = []
|
|
138
|
+
current_item = None
|
|
139
|
+
|
|
140
|
+
while state.index < len(lines):
|
|
141
|
+
line = lines[state.index].strip()
|
|
142
|
+
state.index += 1
|
|
143
|
+
|
|
144
|
+
# Splits the line into values
|
|
145
|
+
raw_values = line.split()
|
|
146
|
+
cast_values = Deserialize.cast_list(raw_values)
|
|
147
|
+
|
|
148
|
+
if len(raw_values) == 1:
|
|
149
|
+
# Updates the section name based off items
|
|
150
|
+
if current_item is not None and rows:
|
|
151
|
+
data[current_item] = cls._transpose(rows)
|
|
152
|
+
rows = []
|
|
153
|
+
|
|
154
|
+
current_item = cast_values[0]
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
# Skip orphaned data
|
|
158
|
+
if current_item is None: continue
|
|
159
|
+
|
|
160
|
+
rows.append(cast_values)
|
|
161
|
+
|
|
162
|
+
if current_item is not None and rows:
|
|
163
|
+
data[current_item] = cls._transpose(rows)
|
|
164
|
+
|
|
165
|
+
return data, state
|
|
166
|
+
|
|
167
|
+
@classmethod
|
|
168
|
+
def _transpose(cls, rows: list[list]) -> list[list]:
|
|
169
|
+
""" Transpose rows into columns """
|
|
170
|
+
# Skips transposing and returns an empty array
|
|
171
|
+
if not rows: return []
|
|
172
|
+
|
|
173
|
+
# Creates a list of columns to transpose.
|
|
174
|
+
num_cols = len(rows[0])
|
|
175
|
+
transposed = [[] for _ in range(num_cols)]
|
|
176
|
+
|
|
177
|
+
for row in rows:
|
|
178
|
+
for col_idx, value in enumerate(row):
|
|
179
|
+
transposed[col_idx].append(value)
|
|
180
|
+
|
|
181
|
+
return transposed
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filename: attributes.py
|
|
3
|
+
|
|
4
|
+
Descriptions:
|
|
5
|
+
Defines the `Attribute` structure
|
|
6
|
+
for the `.ans` files.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from ifemm.constants import FILE_FORMATS
|
|
12
|
+
from ifemm.utilities.errors import FormatNotSupported, AttributeLoadingFailed
|
|
13
|
+
from ifemm.interpreter.magnetic.schema import MagneticData
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AttributeLoader:
|
|
17
|
+
""" Attribute loader for `.ans` files """
|
|
18
|
+
@classmethod
|
|
19
|
+
def load(cls, data: dict, file_type: str) -> MagneticData:
|
|
20
|
+
""" Loads data into type specific structure """
|
|
21
|
+
|
|
22
|
+
file_format = data["format"]
|
|
23
|
+
if file_format not in FILE_FORMATS:
|
|
24
|
+
# File format not supported currently by the interpreter
|
|
25
|
+
raise FormatNotSupported(file_format)
|
|
26
|
+
|
|
27
|
+
match file_type:
|
|
28
|
+
case ".ans":
|
|
29
|
+
# Returns the magnetic data as attribute class
|
|
30
|
+
return MagneticData(data)
|
|
31
|
+
|
|
32
|
+
case _:
|
|
33
|
+
raise AttributeLoadingFailed(file_type)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filename: definitions.py
|
|
3
|
+
|
|
4
|
+
Description:
|
|
5
|
+
Definitions for magnetic solutions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from abc import ABC
|
|
12
|
+
from dataclasses import dataclass, fields
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(slots=True, repr=False)
|
|
16
|
+
class Definition(ABC):
|
|
17
|
+
""" Abstract definition class """
|
|
18
|
+
name: str
|
|
19
|
+
|
|
20
|
+
def __repr__(self):
|
|
21
|
+
""" Returns the loaders direct members """
|
|
22
|
+
attributes = [field.name for field in fields(self)]
|
|
23
|
+
items = ', '.join(attributes)
|
|
24
|
+
if self.name is None:
|
|
25
|
+
return f'Definition({items})'
|
|
26
|
+
return f'{self.name}({items})'
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(slots=True, repr=False)
|
|
30
|
+
class MaterialDefinition(Definition):
|
|
31
|
+
""" Defines a magnetic material """
|
|
32
|
+
name: str
|
|
33
|
+
permeability: tuple[float, float]
|
|
34
|
+
coercive_force: float
|
|
35
|
+
coercive_angle: float
|
|
36
|
+
current_density: tuple[float, float]
|
|
37
|
+
conductivity: float
|
|
38
|
+
lamination_thickness: float
|
|
39
|
+
hysteresis_angle: float
|
|
40
|
+
hysteresis_angle_x: float
|
|
41
|
+
hysteresis_angle_y: float
|
|
42
|
+
lamination_type: int
|
|
43
|
+
lamination_fill: float
|
|
44
|
+
num_strands: int
|
|
45
|
+
wire_diameter: float
|
|
46
|
+
bh_curve: list[tuple[float, float]]
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def define(cls, entry: dict[str, Any]) -> MaterialDefinition:
|
|
50
|
+
""" Self-constructs the magnetic material from entry """
|
|
51
|
+
bh_data = entry.get("<bhpoints>", {})
|
|
52
|
+
bh_curve = [(v[0], v[1]) for v in bh_data.values()] if bh_data else []
|
|
53
|
+
|
|
54
|
+
return cls(
|
|
55
|
+
entry["<blockname>"],
|
|
56
|
+
(entry["<mu_x>"], entry["<mu_y>"]),
|
|
57
|
+
entry["<h_c>"],
|
|
58
|
+
entry["<h_cangle>"],
|
|
59
|
+
(entry["<j_re>"], entry["<j_im>"]),
|
|
60
|
+
entry["<sigma>"],
|
|
61
|
+
entry["<d_lam>"],
|
|
62
|
+
entry["<phi_h>"],
|
|
63
|
+
entry["<phi_hx>"],
|
|
64
|
+
entry["<phi_hy>"],
|
|
65
|
+
entry["<lamtype>"],
|
|
66
|
+
entry["<lamfill>"],
|
|
67
|
+
entry["<nstrands>"],
|
|
68
|
+
entry["<wired>"],
|
|
69
|
+
bh_curve
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(slots=True, repr=False)
|
|
74
|
+
class BoundaryDefinition(Definition):
|
|
75
|
+
""" Defines a boundary condition """
|
|
76
|
+
name: str
|
|
77
|
+
boundary_type: int
|
|
78
|
+
prescribed_a: float
|
|
79
|
+
prescribed_a_x: float
|
|
80
|
+
prescribed_a_y: float
|
|
81
|
+
phase_angle: float
|
|
82
|
+
mixed_c0: float
|
|
83
|
+
mixed_c0_imag: float
|
|
84
|
+
mixed_c1: float
|
|
85
|
+
mixed_c1_imag: float
|
|
86
|
+
mu_ssd: float
|
|
87
|
+
sigma_ssd: float
|
|
88
|
+
inner_angle: float
|
|
89
|
+
outer_angle: float
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def define(cls, entry: dict[str, Any]) -> BoundaryDefinition:
|
|
93
|
+
""" Self-constructs the boundary condition from entry """
|
|
94
|
+
return cls(
|
|
95
|
+
entry["<bdryname>"],
|
|
96
|
+
entry["<bdrytype>"],
|
|
97
|
+
entry["<a_0>"],
|
|
98
|
+
entry["<a_1>"],
|
|
99
|
+
entry["<a_2>"],
|
|
100
|
+
entry["<phi>"],
|
|
101
|
+
entry["<c0>"],
|
|
102
|
+
entry["<c0i>"],
|
|
103
|
+
entry["<c1>"],
|
|
104
|
+
entry["<c1i>"],
|
|
105
|
+
entry["<mu_ssd>"],
|
|
106
|
+
entry["<sigma_ssd>"],
|
|
107
|
+
entry["<innerangle>"],
|
|
108
|
+
entry["<outerangle>"]
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
@dataclass(slots=True, repr=False)
|
|
112
|
+
class CircuitDefinition(Definition):
|
|
113
|
+
""" Defines a circuit """
|
|
114
|
+
name: str
|
|
115
|
+
total_amps_real: float
|
|
116
|
+
total_amps_imag: float
|
|
117
|
+
circuit_type: int
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def define(cls, entry: dict[str, Any]) -> CircuitDefinition:
|
|
121
|
+
""" Self-constructs the circuit from entry """
|
|
122
|
+
return cls(
|
|
123
|
+
entry["<circuitname>"],
|
|
124
|
+
entry["<totalamps_re>"],
|
|
125
|
+
entry["<totalamps_im>"],
|
|
126
|
+
entry["<circuittype>"]
|
|
127
|
+
)
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filename: schema.py
|
|
3
|
+
|
|
4
|
+
Description:
|
|
5
|
+
Magnetic attribute structure
|
|
6
|
+
for FEMM magnetostatics and
|
|
7
|
+
AC simulations.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from math import pi
|
|
11
|
+
from typing import Any
|
|
12
|
+
from scipy.interpolate import NearestNDInterpolator, griddata
|
|
13
|
+
|
|
14
|
+
from numpy import (
|
|
15
|
+
column_stack as np_column_stack,
|
|
16
|
+
linspace as np_linspace,
|
|
17
|
+
meshgrid as np_meshgrid,
|
|
18
|
+
array as np_array,
|
|
19
|
+
gradient as np_gradient,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from ifemm.interpreter.magnetic.definitions import (
|
|
23
|
+
MaterialDefinition,
|
|
24
|
+
BoundaryDefinition,
|
|
25
|
+
CircuitDefinition
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_LENGTH_TO_SI = {
|
|
30
|
+
"meters": 1.0,
|
|
31
|
+
"centimeters": 1e-2,
|
|
32
|
+
"millimeters": 1e-3,
|
|
33
|
+
"inches": 2.54e-2,
|
|
34
|
+
"mils": 2.54e-5,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MagneticData:
|
|
39
|
+
""" Magnetic Attribute Data """
|
|
40
|
+
def __init__(self, data: dict) -> None:
|
|
41
|
+
""" Initializes the class and loads data into attributes """
|
|
42
|
+
self.data = data
|
|
43
|
+
|
|
44
|
+
# Loads variables into attributes
|
|
45
|
+
self._load_top_level()
|
|
46
|
+
self._load_boundaries()
|
|
47
|
+
self._load_materials()
|
|
48
|
+
self._load_circuits()
|
|
49
|
+
|
|
50
|
+
# Creates the A potential map
|
|
51
|
+
self._constructs_potential_map()
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def length_scale(self) -> float:
|
|
55
|
+
""" Conversion factor from model length units to metres. """
|
|
56
|
+
try:
|
|
57
|
+
return _LENGTH_TO_SI[self.length_unit.lower()]
|
|
58
|
+
|
|
59
|
+
except KeyError:
|
|
60
|
+
msg = f"Unsupported FEMM length unit: {self.length_unit}"
|
|
61
|
+
raise ValueError(msg) from None
|
|
62
|
+
|
|
63
|
+
def _constructs_potential_map(self, eps: float = 1e-6) -> None:
|
|
64
|
+
""" Constructs the vector potential map """
|
|
65
|
+
solution = next(iter(self.data["solution"]))
|
|
66
|
+
|
|
67
|
+
# Convert to three lists for each dimension
|
|
68
|
+
self.vector_x = self.data["solution"][solution][0]
|
|
69
|
+
self.vector_y = self.data["solution"][solution][1]
|
|
70
|
+
self.vector_a = self.data["solution"][solution][2]
|
|
71
|
+
|
|
72
|
+
# Converts flux to a-potential for axisymmetric solutions
|
|
73
|
+
if self.problem_type == "axisymmetric":
|
|
74
|
+
converted = []
|
|
75
|
+
for r, a in zip(self.vector_x, self.vector_a):
|
|
76
|
+
if r > eps:
|
|
77
|
+
converted.append(a / (2.0 * pi * r * self.length_scale))
|
|
78
|
+
else:
|
|
79
|
+
converted.append(0.0)
|
|
80
|
+
self.vector_a = converted
|
|
81
|
+
|
|
82
|
+
# Convert to numpy arrays for interpolation
|
|
83
|
+
points = np_column_stack((self.vector_x, self.vector_y))
|
|
84
|
+
values = np_array(self.vector_a)
|
|
85
|
+
|
|
86
|
+
# Create the interpolation function
|
|
87
|
+
self._interpolator = NearestNDInterpolator(points, values)
|
|
88
|
+
|
|
89
|
+
def point_potential(self, x: float, y: float) -> float:
|
|
90
|
+
""" Return the magnetic vector potential A at point (x, y). """
|
|
91
|
+
result = self._interpolator(x, y)
|
|
92
|
+
|
|
93
|
+
# NearestNDInterpolator always returns a value
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
def field_potential(self, resolution: int = 1000) -> tuple[Any, Any, Any]:
|
|
97
|
+
""" Returns the interpolated potential field. """
|
|
98
|
+
x_min, x_max = min(self.vector_x), max(self.vector_x)
|
|
99
|
+
y_min, y_max = min(self.vector_y), max(self.vector_y)
|
|
100
|
+
|
|
101
|
+
# Creates a x and y space
|
|
102
|
+
xi = np_linspace(x_min, x_max, resolution)
|
|
103
|
+
yi = np_linspace(y_min, y_max, resolution)
|
|
104
|
+
x_space, y_space = np_meshgrid(xi, yi)
|
|
105
|
+
|
|
106
|
+
# Interpolate A onto x, y space
|
|
107
|
+
a_grid = griddata(
|
|
108
|
+
(self.vector_x, self.vector_y),
|
|
109
|
+
self.vector_a,
|
|
110
|
+
(x_space, y_space),
|
|
111
|
+
method='linear'
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Returns the X, Y and A spaces
|
|
115
|
+
return x_space, y_space, a_grid
|
|
116
|
+
|
|
117
|
+
def b_field(self, resolution: int = 1000) -> tuple[Any, Any, Any, Any]:
|
|
118
|
+
""" Returns the interpolated B field (Bx, By) from the vector potential A. """
|
|
119
|
+
x_space, y_space, a_grid = self.field_potential(resolution)
|
|
120
|
+
|
|
121
|
+
scale = self.length_scale
|
|
122
|
+
|
|
123
|
+
# Compute gradients in SI metres
|
|
124
|
+
dx = (x_space[0, 1] - x_space[0, 0]) * scale
|
|
125
|
+
dy = (y_space[1, 0] - y_space[0, 0]) * scale
|
|
126
|
+
|
|
127
|
+
da_dx = np_gradient(a_grid, dx, axis=1)
|
|
128
|
+
da_dy = np_gradient(a_grid, dy, axis=0)
|
|
129
|
+
|
|
130
|
+
# B = curl(A)
|
|
131
|
+
bx = da_dy
|
|
132
|
+
by = -da_dx
|
|
133
|
+
|
|
134
|
+
return x_space, y_space, bx, by
|
|
135
|
+
|
|
136
|
+
def _load_circuits(self) -> None:
|
|
137
|
+
""" Loads materials section from the solution """
|
|
138
|
+
circuits = self.data["circuitprops"]
|
|
139
|
+
|
|
140
|
+
for key in circuits:
|
|
141
|
+
# Sets the circuit definition as attribute
|
|
142
|
+
circuit = CircuitDefinition.define(circuits[key])
|
|
143
|
+
setattr(self, circuit.name, circuit)
|
|
144
|
+
|
|
145
|
+
def _load_materials(self) -> None:
|
|
146
|
+
""" Loads materials section from the solution """
|
|
147
|
+
materials = self.data["blockprops"]
|
|
148
|
+
|
|
149
|
+
for key in materials:
|
|
150
|
+
# Sets the material definition as attribute
|
|
151
|
+
material = MaterialDefinition.define(materials[key])
|
|
152
|
+
setattr(self, material.name, material)
|
|
153
|
+
|
|
154
|
+
def _load_boundaries(self) -> None:
|
|
155
|
+
""" Loads boundaries section from the solution """
|
|
156
|
+
boundaries = self.data["bdryprops"]
|
|
157
|
+
|
|
158
|
+
for key in boundaries:
|
|
159
|
+
# Sets the boundary definition as attribute
|
|
160
|
+
boundary = BoundaryDefinition.define(boundaries[key])
|
|
161
|
+
setattr(self, boundary.name, boundary)
|
|
162
|
+
|
|
163
|
+
def _load_top_level(self) -> None:
|
|
164
|
+
""" Loads the top level sections from the solution """
|
|
165
|
+
# File & Version
|
|
166
|
+
self.format_version: float = self.data["format"]
|
|
167
|
+
|
|
168
|
+
# Problem Definition
|
|
169
|
+
self.frequency_hz: float = self.data["frequency"]
|
|
170
|
+
self.solver_precision: float = self.data["precision"]
|
|
171
|
+
self.min_angle_deg: float = self.data["minangle"]
|
|
172
|
+
|
|
173
|
+
# Mesh Settings
|
|
174
|
+
self.model_depth: float = self.data["depth"]
|
|
175
|
+
self.length_unit: str = self.data["lengthunits"]
|
|
176
|
+
self.problem_type: str = self.data["problemtype"]
|
|
177
|
+
self.coordinate_system: str = self.data["coordinates"]
|
|
178
|
+
|
|
179
|
+
# Metadata
|
|
180
|
+
self.comment_text: str = self.data["comment"]
|
ifemm/parser.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filename: main.py
|
|
3
|
+
|
|
4
|
+
Description:
|
|
5
|
+
Domain specific language parser for .ans file
|
|
6
|
+
|
|
7
|
+
Orchestrates parsing the files and producing
|
|
8
|
+
field representations for layer.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from ifemm.core.states import ParserState
|
|
14
|
+
from ifemm.core.deserialization import Deserialize
|
|
15
|
+
from ifemm.core.syntax import BlockExtraction, DataExtraction, SolutionExtraction
|
|
16
|
+
from ifemm.constants import FILE_TYPES, BLOCK_SECTIONS, DATA_SECTIONS, SOLUTION_SECTION
|
|
17
|
+
|
|
18
|
+
from ifemm.utilities.errors import ParserError, FileTypeNotSupported
|
|
19
|
+
from ifemm.interpreter.attributes import AttributeLoader, MagneticData
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Parser:
|
|
23
|
+
""" Parser for .ans file format. """
|
|
24
|
+
@classmethod
|
|
25
|
+
def open(cls, filepath: Path | str ) -> MagneticData:
|
|
26
|
+
""" Parses .ans file into a field representation. """
|
|
27
|
+
if not isinstance(filepath, (str, Path)):
|
|
28
|
+
# Raises error for non supported path type
|
|
29
|
+
msg = f"Invalid path type {type(filepath)!r} for {cls.__name__!r}"
|
|
30
|
+
raise TypeError(msg)
|
|
31
|
+
|
|
32
|
+
# Constructs a path and extracts file type
|
|
33
|
+
path = Path(filepath)
|
|
34
|
+
file_type = path.suffix.lower()
|
|
35
|
+
|
|
36
|
+
# Raises error for non supported file type
|
|
37
|
+
if file_type not in FILE_TYPES: raise FileTypeNotSupported(file_type)
|
|
38
|
+
# Raises error for non supported file type
|
|
39
|
+
|
|
40
|
+
lines = cls._read_lines(filepath)
|
|
41
|
+
data = ParseLines.parse(lines)
|
|
42
|
+
return AttributeLoader.load(data, file_type)
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def _read_lines(filepath_or_file: Path | str) -> list[str]:
|
|
46
|
+
""" Read lines from file path or file-like object. """
|
|
47
|
+
|
|
48
|
+
# Convert to Path and validate
|
|
49
|
+
filepath = Path(filepath_or_file)
|
|
50
|
+
if not filepath.exists():
|
|
51
|
+
raise FileNotFoundError(f"File not found: {filepath}")
|
|
52
|
+
|
|
53
|
+
with filepath.open('r', encoding='utf-8') as f:
|
|
54
|
+
return f.readlines()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ParseLines:
|
|
58
|
+
""" Parses lines for .ans file format. """
|
|
59
|
+
@classmethod
|
|
60
|
+
def parse(cls, lines: list[str]) -> dict:
|
|
61
|
+
""" Parses and extracts logic from raw text into structured results. """
|
|
62
|
+
state = ParserState()
|
|
63
|
+
state.content = {}
|
|
64
|
+
|
|
65
|
+
while state.index < len(lines):
|
|
66
|
+
line = lines[state.index].strip()
|
|
67
|
+
state.index += 1
|
|
68
|
+
|
|
69
|
+
if not line:
|
|
70
|
+
# Skips empty lines
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
# Extracts the solution and value than casts it in primitives
|
|
74
|
+
is_section, section = cls._is_section(line)
|
|
75
|
+
is_value, raw_value = cls._extract_section_value(line)
|
|
76
|
+
|
|
77
|
+
value = Deserialize.cast(raw_value)
|
|
78
|
+
|
|
79
|
+
if section in BLOCK_SECTIONS:
|
|
80
|
+
# Parses block section syntaxes
|
|
81
|
+
data, state = BlockExtraction.extract(lines, value, state)
|
|
82
|
+
state.content[section] = data
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
if section in DATA_SECTIONS:
|
|
86
|
+
# Parses the data section syntaxes
|
|
87
|
+
data, state = DataExtraction.extract(lines, value, state)
|
|
88
|
+
state.content[section] = data
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
if section in SOLUTION_SECTION:
|
|
92
|
+
# Parse the solution section syntaxes
|
|
93
|
+
data, state = SolutionExtraction.extract(lines, state)
|
|
94
|
+
state.content[section] = data
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
if is_section and is_value:
|
|
98
|
+
# Adds the section value under section name
|
|
99
|
+
state.content[section] = value
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
return state.content
|
|
103
|
+
return state.content
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def _is_section(cls, line: str) -> tuple[bool, str]:
|
|
107
|
+
""" Check if line defines a section [name]. """
|
|
108
|
+
line = line.strip()
|
|
109
|
+
if not line.startswith('['):
|
|
110
|
+
return False, ""
|
|
111
|
+
|
|
112
|
+
closing_bracket = line.find(']')
|
|
113
|
+
if closing_bracket == -1:
|
|
114
|
+
msg = "closing bracket not found in-line"
|
|
115
|
+
raise ParserError(cls.__name__, msg)
|
|
116
|
+
|
|
117
|
+
return True, line[1:closing_bracket].lower()
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def _extract_section_value(cls, line: str) -> tuple[bool, str]:
|
|
121
|
+
""" Extract section or subsection values. """
|
|
122
|
+
equal_sign = line.find("=")
|
|
123
|
+
if equal_sign == -1:
|
|
124
|
+
return False, ""
|
|
125
|
+
|
|
126
|
+
return True, line[equal_sign+1:].strip()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filename: parser_errors.py
|
|
3
|
+
|
|
4
|
+
Description:
|
|
5
|
+
Defines the errors classes to ensure
|
|
6
|
+
descriptive error messages.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from ifemm.constants import FILE_FORMATS, FILE_TYPES
|
|
12
|
+
|
|
13
|
+
# pylint: disable=line-too-long
|
|
14
|
+
|
|
15
|
+
# Generic Errors
|
|
16
|
+
class ParserError(ValueError):
|
|
17
|
+
""" Exception for Parser errors when parsing """
|
|
18
|
+
def __init__(self, caller: str, error: str):
|
|
19
|
+
""" Returns a custom error message """
|
|
20
|
+
msg = f"[] '{caller}' raised error: {error}. "
|
|
21
|
+
super().__init__(msg)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Specific errors
|
|
25
|
+
class FailedCasting(ValueError):
|
|
26
|
+
""" Exception for failure during casting """
|
|
27
|
+
CODE = "E001"
|
|
28
|
+
|
|
29
|
+
def __init__(self, text: Any, error: str):
|
|
30
|
+
""" Returns a failed casting error """
|
|
31
|
+
msg = f"[{self.CODE}] Failed to cast {text!r} as Python primitive, error: {error!r}"
|
|
32
|
+
super().__init__(msg)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class FileTypeNotSupported(Exception):
|
|
36
|
+
""" Exception for file type not supported """
|
|
37
|
+
CODE = "E002"
|
|
38
|
+
|
|
39
|
+
def __init__(self, file_type: str):
|
|
40
|
+
""" Returns a file type not supported error """
|
|
41
|
+
msg = f"[{self.CODE}] Failed to parse {file_type!r} as not in supported list {FILE_TYPES!r}"
|
|
42
|
+
super().__init__(msg)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class FormatNotSupported(Exception):
|
|
46
|
+
""" Exception for format not supported """
|
|
47
|
+
CODE = "E003"
|
|
48
|
+
|
|
49
|
+
def __init__(self, file_format: str):
|
|
50
|
+
""" Returns a format not supported error """
|
|
51
|
+
msg = f"[{self.CODE}] Failed to parse {file_format!r} as not in supported list {FILE_FORMATS!r}"
|
|
52
|
+
super().__init__(msg)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class AttributeLoadingFailed(Exception):
|
|
56
|
+
""" Exception for attribute loading failing """
|
|
57
|
+
CODE = "E004"
|
|
58
|
+
|
|
59
|
+
def __init__(self, file_type: str):
|
|
60
|
+
""" Returns a loading failure error """
|
|
61
|
+
msg = f"[{self.CODE}] Failed to load {file_type!r} attributes into attribute structure"
|
|
62
|
+
super().__init__(msg)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ifemm
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A python library for interpreting FEMM solution files
|
|
5
|
+
Project-URL: Homepage, https://github.com/wgbowley/ifemm
|
|
6
|
+
Project-URL: Bug Tracker, https://github.com/wgbowley/ifemm/issues
|
|
7
|
+
Author-email: William Bowley <wgrantbowley@gmail.com>
|
|
8
|
+
Maintainer-email: William Bowley <wgrantbowley@gmail.com>
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 William Bowley
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: FEM,FEMM,Parsing,format
|
|
32
|
+
Classifier: Intended Audience :: Developers
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Operating System :: OS Independent
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Classifier: Topic :: Scientific/Engineering
|
|
37
|
+
Requires-Python: >=3.10
|
|
38
|
+
Requires-Dist: numpy>=1.24.0
|
|
39
|
+
Requires-Dist: scipy>=1.10.0
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
<!--
|
|
43
|
+
Color Palette:
|
|
44
|
+
#FFFFFF - pure white
|
|
45
|
+
#00FFFF - pure, highly saturated shade of Cyan
|
|
46
|
+
|
|
47
|
+
It's a simple piece of kit, but being able to integrate
|
|
48
|
+
FEMM solutions directly into the pipeline is extremely
|
|
49
|
+
useful.
|
|
50
|
+
|
|
51
|
+
- William Bowley, 2026-08-17
|
|
52
|
+
|
|
53
|
+
P.S: Thanks for downloading the ifemm repository `▽`ʃ♡
|
|
54
|
+
-->
|
|
55
|
+
|
|
56
|
+
### Overview
|
|
57
|
+
|
|
58
|
+

|
|
59
|
+

|
|
60
|
+

|
|
61
|
+
[](https://pepy.tech/projects/femminterpreter)
|
|
62
|
+
|
|
63
|
+
A Python library for interpreting Finite Element Method Magnetic (FEMM) files and exposing FEMM solution data
|
|
64
|
+
through a Python attribute-based interface. `ifemm` exposes the `A-field` as `A(x,y)`, independent of planar or axisymmetric coordinate systems.
|
|
65
|
+
|
|
66
|
+
#### Proposed Integration
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
FEMM Setup & Solve → FEMM (.ans) → ifemm → Reduced Order Models / Analytical Models
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
### Example
|
|
75
|
+
|
|
76
|
+
An example of a dipole being plotted can be found in [/examples](examples/) with `.ans` and `.py` files.
|
|
77
|
+
|
|
78
|
+
<div align="center">
|
|
79
|
+
<img
|
|
80
|
+
src="https://raw.githubusercontent.com/wgbowley/FEMMInterpreter/refs/heads/main/media/planar_llc_b_field.png"
|
|
81
|
+
alt="B-field of transformer"
|
|
82
|
+
style="max-width: 600px"
|
|
83
|
+
>
|
|
84
|
+
<p>
|
|
85
|
+
<em> B-field of a planar LLC transformer from FEMM (.ans) </em>
|
|
86
|
+
</p>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
### Quick Start
|
|
92
|
+
|
|
93
|
+
```py
|
|
94
|
+
from ifemm import Parser
|
|
95
|
+
|
|
96
|
+
# Imports the parser and parses the .ans file
|
|
97
|
+
PATH = "examples/magnetostatic.ans"
|
|
98
|
+
data = Parser.open(PATH)
|
|
99
|
+
|
|
100
|
+
# Result as a float with implicit unit of wb/length_unit
|
|
101
|
+
a_potential = data.point_potential(0, 0)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
### Installation
|
|
107
|
+
|
|
108
|
+
To install,
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
pip install ifemm
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
### Documentation
|
|
117
|
+
|
|
118
|
+
Full documentation is available in the [`docs/`](https://github.com/wgbowley/ifemm/tree/main/docs) folder, including API reference, changelog, and contributors.
|
|
119
|
+
|
|
120
|
+
---
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
ifemm/__init__.py,sha256=Kggl2PH-Ua9TIBk73jLUI-4VrMG5JzAQcUAfSoRbUHA,147
|
|
2
|
+
ifemm/constants.py,sha256=6452Yild1ElbwkJN9CJM0zNrfZfF7szEaYqf8JVh0QA,1009
|
|
3
|
+
ifemm/parser.py,sha256=arg3s1XrFc3dud5crcOeTJF9aFm9w7RYfnj07QRPl6Q,4297
|
|
4
|
+
ifemm/core/__init__.py,sha256=d9hDTekERYUfOF1O0Ur9blpDDx2_w34VQT6p0KDDryA,56
|
|
5
|
+
ifemm/core/deserialization.py,sha256=TEKWAH-SktCI_0a7wMnQQdKVn1vwCTVOrLvLjHOs51k,2454
|
|
6
|
+
ifemm/core/states.py,sha256=lA033cCIj1aVEepIU5RPlavl8ii8YR2_ZD63c7RvZKg,374
|
|
7
|
+
ifemm/core/syntax.py,sha256=agtoF1km1oqPzuIaJfJmkWzDnEypGBVMzKGeiLi5W8w,5593
|
|
8
|
+
ifemm/interpreter/__init__.py,sha256=rH5wLOJMxWUN1WaCQY8hINVBi4pzqmtGLBV3PMx3bR0,62
|
|
9
|
+
ifemm/interpreter/attributes.py,sha256=KKR0oJ2FV3DTpnc0pM1Yus6JIrPELW5ATi0_XT21Myk,968
|
|
10
|
+
ifemm/interpreter/magnetic/__init__.py,sha256=72nvrj9WhEkcmKyV2rOR0Th3qK5_dg8wTXZ49mnQw6g,71
|
|
11
|
+
ifemm/interpreter/magnetic/definitions.py,sha256=iDPgml7A7YT9-fZGgj8Oa0962yxuFoHFSojknPm7wd8,3479
|
|
12
|
+
ifemm/interpreter/magnetic/schema.py,sha256=tOh7oFu0vqSWdQ1KotFCmLi7hrgK-EI8X7QA5xI1gYQ,5882
|
|
13
|
+
ifemm/utilities/__init__.py,sha256=df7bmOOqt3lTii-yT4Z5ywRaf7RSjg2RSESm3DNuFEk,59
|
|
14
|
+
ifemm/utilities/errors.py,sha256=WHSQS6qVqEjL5EKVQRGw7YRD4wsZRCm0vUNVtIkWaA0,1855
|
|
15
|
+
ifemm-0.2.0.dist-info/METADATA,sha256=bd0rgi2oT62lO94haArd_9haHQ52CPrJr3RfijsPzOQ,4066
|
|
16
|
+
ifemm-0.2.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
17
|
+
ifemm-0.2.0.dist-info/licenses/LICENSE,sha256=PB4SZBAK73vxATTUsk5y_0XiWa0lHuZREvj3y4Syr04,1071
|
|
18
|
+
ifemm-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 William Bowley
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|