Compare commits

..

No commits in common. "master" and "relase" have entirely different histories.

11 changed files with 121 additions and 184 deletions

3
.gitignore vendored
View File

@ -1,5 +1,5 @@
*.pyc *.pyc
__pycache__/ __pycache__
*.idea *.idea
@ -7,7 +7,6 @@ build/
dist/ dist/
*.egg-info/ *.egg-info/
.mypy_cache/ .mypy_cache/
.pytest_cache/
*.swp *.swp
*.swo *.swo

View File

@ -6,13 +6,13 @@
### Links ### Links
- [Source repository](https://mpxd.net/code/jan/g85) - [Source repository](https://mpxd.net/code/jan/g85)
- [PyPI](https://pypi.org/project/g85) - [PyPI](https://pypi.org/project/g85)
- [Github mirror](https://github.com/anewusername/g85)
## Installation ## Installation
Requirements: Requirements:
* python >= 3.10 (written and tested with 3.11) * python >= 3.7 (written and tested with 3.9)
* numpy
Install with pip: Install with pip:

View File

@ -1 +0,0 @@
../LICENSE.md

View File

@ -1 +0,0 @@
../README.md

4
g85/VERSION.py Normal file
View File

@ -0,0 +1,4 @@
""" VERSION defintion. THIS FILE IS MANUALLY PARSED BY setup.py and REQUIRES A SPECIFIC FORMAT """
__version__ = '''
0.1
'''.strip()

View File

@ -1,9 +1,3 @@
from .main import ( from .main import Map, Device
Map as Map, from .read import read
Device as Device, from .write import write
)
from .read import read as read
from .write import write as write
__author__ = 'Jan Petykiewicz'
__version__ = '0.7'

View File

@ -1,3 +1,4 @@
from typing import Dict, List, Tuple, Union, Optional
import datetime import datetime
from collections import Counter from collections import Counter
from dataclasses import dataclass, field from dataclasses import dataclass, field
@ -7,25 +8,25 @@ from itertools import chain
@dataclass @dataclass
class Device: class Device:
BinType: str BinType: str
NullBin: str | int NullBin: Union[str, int]
ProductId: str | None = None ProductId: Optional[str] = None
LotId: str | None = None LotId: Optional[str] = None
WaferSize: float | None = None WaferSize: Optional[float] = None
CreateDate: datetime.datetime | None = None CreateDate: Optional[datetime.datetime] = None
DeviceSizeX: float | None = None DeviceSizeX: Optional[float] = None
DeviceSizeY: float | None = None DeviceSizeY: Optional[float] = None
SupplierName: str | None = None SupplierName: Optional[str] = None
OriginLocation: int | None = None OriginLocation: Optional[int] = None
MapType: str = 'Array' MapType: str = 'Array'
Orientation: float = 0 Orientation: float = 0
reference_xy: tuple[int, int] | None = None reference_xy: Optional[Tuple[int, int]] = None
bin_pass: dict[int | str, bool] = field(default_factory=dict) # Is this bin passing? bin_pass: Dict[Union[int, str], bool] = field(default_factory=dict) # Is this bin passing?
map: list[list[int]] | list[list[str]] = field(default_factory=list) # The actual map map: Union[List[List[int]], List[List[str]]] = field(default_factory=list) # The actual map
data_misc: dict[str, str] = field(default_factory=dict) # <Data attribs> # Map attribs: MapName, MapVersion
supplier_data: dict[str, str] = field(default_factory=dict) # <SupplierData attribs> # SupplierData attribs: ProductCode, RecipeName
misc: dict[str, str] = field(default_factory=dict) # Any unexpected fields go here misc: Dict[str, str] = field(default_factory=dict) # Any unexpected fields go here
@property @property
def Rows(self) -> int: def Rows(self) -> int:
@ -45,9 +46,9 @@ class Device:
class Map: class Map:
xmlns: str = 'http://www.semi.org' xmlns: str = 'http://www.semi.org'
FormatRevision: str = "SEMI G85 0703" FormatRevision: str = "SEMI G85 0703"
SubstrateType: str | None = None SubstrateType: Optional[str] = None
SubstrateId: str | None = None SubstrateId: Optional[str] = None
devices: list[Device] = field(default_factory=list) devices: List[Device] = field(default_factory=list)
misc: dict[str, str] = field(default_factory=dict) # Any unexpected fields go here misc: Dict[str, str] = field(default_factory=dict) # Any unexpected fields go here

View File

@ -1,4 +1,4 @@
from typing import TextIO, Any from typing import List, Union, TextIO, Any
import logging import logging
import datetime import datetime
from dataclasses import fields from dataclasses import fields
@ -10,7 +10,7 @@ from .main import Map, Device
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def read(stream: TextIO) -> list[Map]: def read(stream: TextIO) -> List[Map]:
tree = ElementTree.parse(stream) tree = ElementTree.parse(stream)
el_root = tree.getroot() el_root = tree.getroot()
@ -21,7 +21,7 @@ def read(stream: TextIO) -> list[Map]:
return maps return maps
def read_wmaps(el_root: ElementTree.Element) -> list[Map]: def read_wmaps(el_root: ElementTree.Element) -> List[Map]:
map_fields = [ff.name for ff in fields(Map)] map_fields = [ff.name for ff in fields(Map)]
maps = [] maps = []
for el_map in el_root: for el_map in el_root:
@ -41,7 +41,7 @@ def read_wmaps(el_root: ElementTree.Element) -> list[Map]:
return maps return maps
def read_devices(el_map: ElementTree.Element) -> list[Device]: def read_devices(el_map: ElementTree.Element) -> List[Device]:
dev_fields = [ff.name for ff in fields(Device)] dev_fields = [ff.name for ff in fields(Device)]
devices = [] devices = []
for el_device in el_map: for el_device in el_map:
@ -50,7 +50,7 @@ def read_devices(el_map: ElementTree.Element) -> list[Device]:
continue continue
bin_type = el_device.attrib['BinType'] bin_type = el_device.attrib['BinType']
null_bin: int | str null_bin: Union[int, str]
if bin_type == 'Decimal': if bin_type == 'Decimal':
null_bin = int(el_device.attrib['NullBin']) null_bin = int(el_device.attrib['NullBin'])
else: else:
@ -58,24 +58,22 @@ def read_devices(el_map: ElementTree.Element) -> list[Device]:
device = Device(BinType=bin_type, NullBin=null_bin) device = Device(BinType=bin_type, NullBin=null_bin)
val: Any
for key, val in el_device.attrib.items(): for key, val in el_device.attrib.items():
if key in ('BinType', 'NullBin'): if key in ('BinType', 'NullBin'):
continue continue
parsed_val: Any
if key in ('WaferSize', 'DeviceSizeX', 'DeviceSizeY', 'Orientation'): if key in ('WaferSize', 'DeviceSizeX', 'DeviceSizeY', 'Orientation'):
parsed_val = float(val) val = float(val)
elif key in ('OriginLocation',): elif key in ('OriginLocation',):
parsed_val = int(val) val = int(val)
elif key == 'CreateDate': elif key == 'CreateDate':
parsed_val = datetime.datetime.strptime(val + '000', '%Y%m%d%H%M%S%f') val = datetime.datetime.strptime(val + '000', '%Y%m%d%H%M%S%f')
else:
parsed_val = val
if key in dev_fields and key[0].isupper(): if key in dev_fields and key[0].isupper():
setattr(device, key, parsed_val) setattr(device, key, val)
else: else:
device.misc[key] = parsed_val device.misc[key] = val
for el_entry in el_device: for el_entry in el_device:
tag = _tag(el_entry) tag = _tag(el_entry)
@ -98,12 +96,7 @@ def read_devices(el_map: ElementTree.Element) -> list[Device]:
f'with attributes {el_entry.attrib}') f'with attributes {el_entry.attrib}')
continue continue
bin_code: int | str bin_code = attrib['BinCode']
if bin_type == 'Decimal':
bin_code = int(attrib['BinCode'])
else:
bin_code = attrib['BinCode']
if bin_code in device.bin_pass: if bin_code in device.bin_pass:
logger.error(f'Bin code {bin_code} was repeated; ignoring later entry!') logger.error(f'Bin code {bin_code} was repeated; ignoring later entry!')
continue continue
@ -111,23 +104,17 @@ def read_devices(el_map: ElementTree.Element) -> list[Device]:
device.bin_pass[bin_code] = attrib['BinQuality'].lower() == 'pass' device.bin_pass[bin_code] = attrib['BinQuality'].lower() == 'pass'
elif tag == 'Data': elif tag == 'Data':
data_strs = [read_row(rr) for rr in el_entry] data_strs = [read_row(rr) for rr in el_entry]
data: list[list[str]] | list[list[int]] data: Union[List[List[str]], List[List[int]]]
if device.BinType == 'Decimal': if device.BinType == 'Decimal':
data = [[int(vv) for vv in rr] for rr in data_strs] data = [[int(vv) for vv in rr] for rr in data_strs]
else: else:
data = data_strs data = data_strs
device.map = data device.map = data
for key, value in attrib.items():
device.data_misc[key] = value
elif tag == 'SupplierData':
for key, value in attrib.items():
device.supplier_data[key] = value
devices.append(device) devices.append(device)
return devices return devices
def read_row(el_row: ElementTree.Element) -> list[str]: def read_row(el_row: ElementTree.Element) -> List[str]:
assert _tag(el_row) == 'Row' assert _tag(el_row) == 'Row'
row_stripped = (el_row.text or '').strip() row_stripped = (el_row.text or '').strip()
@ -139,7 +126,7 @@ def read_row(el_row: ElementTree.Element) -> list[str]:
def _tag(element: ElementTree.Element) -> str: def _tag(element: ElementTree.Element) -> str:
""" '''
Get the element's tag, excluding any namespaces. Get the element's tag, excluding any namespaces.
""" '''
return element.tag.split('}')[-1] return element.tag.split('}')[-1]

View File

@ -1,5 +1,4 @@
from typing import TextIO, cast from typing import Sequence, Tuple, List, TextIO, Union
from collections.abc import Sequence
import logging import logging
import math import math
from dataclasses import fields from dataclasses import fields
@ -11,15 +10,12 @@ from .main import Map, Device
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class G85Error(Exception):
pass
# Hack to directly pass through <![CDATA[...]]> # Hack to directly pass through <![CDATA[...]]>
def _escape_cdata(text: str) -> str: def _escape_cdata(text):
if text.startswith('<![CDATA[') and text.endswith(']]>'): if text.startswith('<![CDATA[') and text.endswith(']]>'):
return text return text
return _original_escape_cdata(text) else:
return _original_escape_cdata(text)
_original_escape_cdata = ElementTree._escape_cdata # type: ignore _original_escape_cdata = ElementTree._escape_cdata # type: ignore
@ -45,11 +41,8 @@ def write_wmap(wmap: Map, el_root: ElementTree.Element) -> None:
map_fields = [ff.name for ff in fields(wmap)] map_fields = [ff.name for ff in fields(wmap)]
for field in map_fields: for field in map_fields:
if field[0].isupper() or field == 'xmlns': if field[0].isupper():
val = getattr(wmap, field) el_map.set(field, getattr(wmap, field))
if val is None:
continue
el_map.set(field, val)
for key, value in wmap.misc.items(): for key, value in wmap.misc.items():
if key[0].isupper() and key in map_fields: if key[0].isupper() and key in map_fields:
continue continue
@ -68,7 +61,7 @@ def write_devices(devices: Sequence[Device], el_map: ElementTree.Element) -> Non
# Row data prep # Row data prep
if device.map is None: if device.map is None:
raise G85Error(f'No _data for device pformat({device})') raise Exception(f'No _data for device pformat({device})')
is_decimal = device.BinType == 'Decimal' is_decimal = device.BinType == 'Decimal'
row_texts, bin_length = prepare_data(device.map, decimal=is_decimal) row_texts, bin_length = prepare_data(device.map, decimal=is_decimal)
@ -88,9 +81,8 @@ def write_devices(devices: Sequence[Device], el_map: ElementTree.Element) -> Non
el_bin.set('BinQuality', 'Pass' if passed else 'Fail') el_bin.set('BinQuality', 'Pass' if passed else 'Fail')
el_bin.set('BinCount', str(bin_counts[bin_code])) el_bin.set('BinCount', str(bin_counts[bin_code]))
el_data = ElementTree.SubElement(el_device, 'Data')
for row_text in row_texts: for row_text in row_texts:
el_row = ElementTree.SubElement(el_data, 'Row') el_row = ElementTree.SubElement(el_device, 'Row')
el_row.text = f'<![CDATA[{row_text}]]>' el_row.text = f'<![CDATA[{row_text}]]>'
# Device attribs # Device attribs
@ -98,8 +90,6 @@ def write_devices(devices: Sequence[Device], el_map: ElementTree.Element) -> Non
for field in dev_fields: for field in dev_fields:
if field[0].isupper(): if field[0].isupper():
val = getattr(device, field) val = getattr(device, field)
if val is None:
continue
if field in ('WaferSize', 'DeviceSizeX', 'DeviceSizeY', 'Orientation'): if field in ('WaferSize', 'DeviceSizeX', 'DeviceSizeY', 'Orientation'):
val = f'{val:g}' val = f'{val:g}'
@ -107,8 +97,6 @@ def write_devices(devices: Sequence[Device], el_map: ElementTree.Element) -> Non
val = f'{val:d}' val = f'{val:d}'
elif field == 'CreateDate': elif field == 'CreateDate':
val = val.strftime('%Y%m%d%H%M%S%f')[:-3] val = val.strftime('%Y%m%d%H%M%S%f')[:-3]
elif field == 'NullBin' and device.BinType == 'Decimal':
val = f'{val:d}'
el_device.set(field, val) el_device.set(field, val)
@ -117,34 +105,27 @@ def write_devices(devices: Sequence[Device], el_map: ElementTree.Element) -> Non
continue continue
el_device.set(key, value) el_device.set(key, value)
for key, value in device.data_misc.items():
el_data.set(key, value)
if device.supplier_data: def prepare_data(data: List[List[Union[str, int]]], decimal: bool) -> Tuple[List[str], int]:
el_suppdata = ElementTree.SubElement(el_device, 'SupplierData')
for key, value in device.data_misc.items():
el_suppdata.set(key, value)
def prepare_data(data: list[list[str]] | list[list[int]], decimal: bool) -> tuple[list[str], int]:
is_char = isinstance(data[0][0], str) is_char = isinstance(data[0][0], str)
row_texts = []
if is_char: if is_char:
data = cast(list[list[str]], data)
char_len = len(data[0][0]) char_len = len(data[0][0])
for srow in data: else:
if char_len == 1:
row_text = ''.join(srow)
else:
row_text = ' '.join(srow) + ' '
row_texts.append(row_text)
return row_texts, char_len
else: # noqa: RET505
data = cast(list[list[int]], data)
max_value = max(max(rr) for rr in data) max_value = max(max(rr) for rr in data)
max_digits = math.ceil(math.log10(max_value)) max_digits = math.ceil(math.log10(max_value))
for irow in data:
row_text = ' '.join(str(vv).zfill(max_digits) for vv in irow) + ' ' row_texts = []
row_texts.append(row_text) for row in data:
if is_char and char_len == 1:
row_text = ''.join(row)
elif is_char:
row_text = ' '.join(row) + ' '
else:
row_text = ' '.join(str(vv).zfill(max_digits) for vv in row) + ' '
row_texts.append(row_text)
if is_char:
return row_texts, char_len
else:
return row_texts, max_digits return row_texts, max_digits

View File

@ -1,78 +0,0 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "g85"
description = "G85 wafer map reader / writer"
readme = "README.md"
license = { file = "LICENSE.md" }
authors = [
{ name="Jan Petykiewicz", email="jan@mpxd.net" },
]
homepage = "https://mpxd.net/code/jan/g85"
repository = "https://mpxd.net/code/jan/g85"
keywords = [
"design",
"CAD",
"EDA",
"electronics",
"photonics",
"IC",
"mask",
"wafer",
"map",
"G85",
"wmap",
]
classifiers = [
"Programming Language :: Python :: 3",
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Intended Audience :: Information Technology",
"Intended Audience :: Manufacturing",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
]
requires-python = ">=3.10"
dynamic = ["version"]
dependencies = [
]
[tool.hatch.version]
path = "g85/__init__.py"
[tool.ruff]
exclude = [
".git",
"dist",
]
line-length = 145
indent-width = 4
lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
lint.select = [
"NPY", "E", "F", "W", "B", "ANN", "UP", "SLOT", "SIM", "LOG",
"C4", "ISC", "PIE", "PT", "RET", "TCH", "PTH", "INT",
"ARG", "PL", "R", "TRY",
"G010", "G101", "G201", "G202",
"Q002", "Q003", "Q004",
]
lint.ignore = [
#"ANN001", # No annotation
"ANN002", # *args
"ANN003", # **kwargs
"ANN401", # Any
"ANN101", # self: Self
"SIM108", # single-line if / else assignment
"RET504", # x=y+z; return x
"PIE790", # unnecessary pass
"ISC003", # non-implicit string concatenation
"C408", # dict(x=y) instead of {'x': y}
"PLR09", # Too many xxx
"PLR2004", # magic number
"PLC0414", # import x as x
"TRY003", # Long exception message
]

51
setup.py Normal file
View File

@ -0,0 +1,51 @@
#!/usr/bin/env python3
from setuptools import setup, find_packages
with open('README.md', 'r') as f:
long_description = f.read()
with open('g85/VERSION.py', 'rt') as f:
version = f.readlines()[2].strip()
setup(
name='g85',
version=version,
description='G85 wafer map reader / writer',
long_description=long_description,
long_description_content_type='text/markdown',
author='Jan Petykiewicz',
author_email='jan@mpxd.net',
url='https://mpxd.net/code/jan/g85',
packages=find_packages(),
package_data={
'g85': ['py.typed'],
},
install_requires=[
'numpy',
],
classifiers=[
'Programming Language :: Python :: 3',
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'Intended Audience :: Manufacturing',
'Intended Audience :: Science/Research',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)',
],
keywords=[
'design',
'CAD',
'EDA',
'electronics',
'photonics',
'IC',
'mask',
'wafer',
'map',
'G85',
'wmap',
],
)