Compare commits

..

55 commits

Author SHA1 Message Date
jan
08ab2b41d5 bump version to v0.11 2021-01-08 09:44:19 -08:00
jan
ba6b04d82d fix version import 2021-01-08 09:43:47 -08:00
f9b8bf823b bump version to v0.10 2020-11-03 01:28:20 -08:00
b996f5f27e use VERSION.py instead of plain VERSION file
reduces reliance on package_data
2020-11-03 01:27:47 -08:00
60f879a1ad style fixes (per flake8) 2020-10-16 19:00:00 -07:00
4a878aa7bf fix type definition for modals 2020-10-16 18:59:45 -07:00
c1b79485a7 Bump version to v0.9 2020-09-10 20:04:15 -07:00
3ca999fa2e documentation updates 2020-09-10 20:03:19 -07:00
3627b63658 Make records store their associated Property records.
Previously, `Property` records were (incorrectly) just associated with
the library or cell.

This requires a change to how `CellName`s are handled by `OasisLayout`,
since they can have associated properties. They now have their own
non-record object (like `XName`s did before) which holds the properties.

There is also now a `FileModals.property_target` attribute which tracks
which record new `Property` rescords should be associated with.
2020-09-10 20:03:01 -07:00
167b16e1c9 fix writing of property values 2020-09-10 19:55:03 -07:00
2c2013a0fc move typing imports to top of file 2020-09-10 19:54:25 -07:00
a80ac6199a Record type 17 (Placement) should not allow modal angle or magnification 2020-09-10 19:54:03 -07:00
jan
97f2bb1238 add docs to gitignore 2020-07-03 13:27:57 -07:00
jan
bc15a66ecc remove extra assignments to *_count and *_vector, and adjust validity checks 2020-07-03 13:27:34 -07:00
jan
5ac774c386 drop OS tags from package 2020-07-03 13:22:29 -07:00
4b7b6b82c1 bump version to v0.8 2020-05-20 21:09:15 -07:00
94555c1b6e Update modal coordinates even if we are in relative mode 2020-05-20 21:07:29 -07:00
86c1e4cd3b bump version to v0.7 2020-05-19 00:52:52 -07:00
fdf5e9f598 enable type checking for downstream 2020-05-19 00:51:40 -07:00
fab80c8517 cosmetic change to code 2020-05-19 00:51:24 -07:00
492d6416db Docstring updates: CTrapezoid info, Polygon+Path point_list description improvement, and better description of where x and y point to 2020-05-19 00:51:10 -07:00
aaef122178 fixup array equality checking in the case where _USE_NUMPY is false but we somehow still get an ndarray
mostly happens during testing
2020-05-19 00:49:42 -07:00
55638fcde5 fix placement rotation (float modulo int was always returning 0??) 2020-05-19 00:48:22 -07:00
99283aaaf0 enable passing in str where an AString or NString is needed. 2020-05-19 00:47:17 -07:00
705926d443 Add UnfilledModalError, records.verify_modal(), and .get_*() methods.
The .get_*() methods are used to verify that we aren't reading from a
pattern with un-filled modals.

The GeometryMixin class was also added here and provides some additional
convenience methods: get_xy() to get an (x,y) tuple and
get_layer_tuple() to get a (layer, datatype) tuple.
2020-05-19 00:42:42 -07:00
e4a62a0f32 fix write_interval for bounded intervals 2020-05-19 00:22:53 -07:00
715fe7ea24 fix non-numpy write_point_list 2020-05-19 00:21:50 -07:00
d25f3f1598 enable str() casts on NString and AString 2020-05-19 00:21:23 -07:00
e909aa958d fix equality for things which may or may not be numpy arrays 2020-05-19 00:20:59 -07:00
411012079d bifurctae read_bool_byte and write_bool_byte into _np_* and _py_* variants 2020-05-19 00:20:38 -07:00
f15499030d only write the first byte -- probably no different but clearer 2020-05-19 00:17:31 -07:00
73310fe993 import repetitions at top level 2020-05-19 00:16:29 -07:00
3a2d889360 add mypy_cache to gitignore 2020-05-17 17:27:11 -07:00
3b01117329 Bump version number to v0.6 2020-04-18 15:55:20 -07:00
4c5f649eec Force mypy to ignore a bunch of simple situations it's not smart enough to reason about 2020-04-18 15:52:36 -07:00
25b2cecec9 Minor changes to satisfy type checker 2020-04-18 15:50:58 -07:00
c9adca61b6 get_pathext may return None 2020-04-18 15:44:44 -07:00
1a259a1c19 Improve ctrapezoid validity checking
width/height might be None; check them against each other only if they
aren't. Also, perform checks after dedup/adjust.
2020-04-18 15:43:25 -07:00
3af40dd1bc Use a dummy unit of -1 when reading (to satisfy type checker) 2020-04-18 15:41:44 -07:00
8fb2f2b594 fix attempted access to xname.string -> xname.bstring 2020-04-18 15:40:35 -07:00
cfb3e90d71 Fix XYmode.absolute 2020-04-18 15:39:27 -07:00
7f0c46525e improve type annotations 2020-04-18 15:38:52 -07:00
e9cf010f54 fix read_u32 2020-04-18 15:35:42 -07:00
06de10062d Additional error checking 2020-04-18 15:35:29 -07:00
62fca39a69 Modernize comments and type annotations. Ugly commit with a couple code fixes:
- modal variable name change: path_halfwidth -> path_half_width
- Fixed `hasattr(other, 'as_list')` calls in __eq__() functions
2020-04-18 03:08:08 -07:00
6f2200c5ed Faster/simpler cumsum approach in read_point_list
Reqires a special case for ndarrays in dedup_field() -- probably a good
idea anyways if user gives us an ndarray
2020-04-18 03:00:36 -07:00
e046af8ce8 Handle more error cases 2020-04-18 02:54:46 -07:00
533c85b249 Fix conditional for writing real-typed properties 2020-04-18 01:42:14 -07:00
bb9ebfc8f9 Generate a warning instead of printing 2020-04-18 01:37:53 -07:00
f4eeb50a6f Fix incorrect calls to .write() 2020-04-18 01:31:36 -07:00
58b4f4a40f Some minor docstring/readme updates 2020-04-17 13:51:43 -07:00
b5a7c9a7ad fix non-numpy read_bool_byte 2020-04-16 22:41:53 -07:00
deb0fe3bef Bump version to 0.5 2019-09-28 11:23:12 -07:00
e0c2947865 trim down classifiers 2019-09-28 11:22:43 -07:00
e514ade2b1 Use fatamorgana/VERSION file to single-source version number
`import fatamorgana` inside setup.py could break if dependencies weren't
satisfied
2019-09-28 11:22:27 -07:00
11 changed files with 2006 additions and 1327 deletions

29
.flake8 Normal file
View file

@ -0,0 +1,29 @@
[flake8]
ignore =
# E501 line too long
E501,
# W391 newlines at EOF
W391,
# E241 multiple spaces after comma
E241,
# E302 expected 2 newlines
E302,
# W503 line break before binary operator (to be deprecated)
W503,
# E265 block comment should start with '# '
E265,
# E123 closing bracket does not match indentation of opening bracket's line
E123,
# E124 closing bracket does not match visual indentation
E124,
# E221 multiple spaces before operator
E221,
# E201 whitespace after '['
E201,
# E741 ambiguous variable name 'I'
E741,
per-file-ignores =
# F401 import without use
*/__init__.py: F401,

2
.gitignore vendored
View file

@ -1,7 +1,9 @@
*.pyc *.pyc
__pycache__ __pycache__
*.idea *.idea
.mypy_cache/
build build
dist dist
fatamorgana.egg-info fatamorgana.egg-info
docs

View file

@ -1,2 +1,3 @@
include README.md include README.md
include LICENSE.md include LICENSE.md
include fatamorgana/VERSION

View file

@ -1,4 +1,4 @@
# fatamorgana # fatamorgana
**fatamorgana** is a Python package for reading and writing OASIS format layout files. **fatamorgana** is a Python package for reading and writing OASIS format layout files.
@ -6,7 +6,7 @@
**Capabilities:** **Capabilities:**
* This package is a work-in-progress and is largely untested -- it works for * This package is a work-in-progress and is largely untested -- it works for
the tasks I usually use it for, but I can't guarantee I've even the tasks I usually use it for, but I can't guarantee I've even
tried the features you happen to use! Use at your own risk! tried the features you happen to use! Use at your own risk!
* Interfaces and datastructures are subject to change! * Interfaces and datastructures are subject to change!
* That said the following work for me: * That said the following work for me:
@ -26,12 +26,12 @@
Install with pip from PyPi (preferred): Install with pip from PyPi (preferred):
```bash ```bash
pip install fatamorgana pip3 install fatamorgana
``` ```
Install directly from git repository: Install directly from git repository:
```bash ```bash
pip install git+https://mpxd.net/code/jan/fatamorgana.git@release pip3 install git+https://mpxd.net/code/jan/fatamorgana.git@release
``` ```
## Documentation ## Documentation
@ -53,7 +53,7 @@ Read an OASIS file and write it back out:
with open('test.oas', 'rb') as f: with open('test.oas', 'rb') as f:
layout = fatamorgana.OasisLayout.read(f) layout = fatamorgana.OasisLayout.read(f)
with open('test_write.oas', 'wb') as f: with open('test_write.oas', 'wb') as f:
layout.write(f) layout.write(f)
``` ```

4
fatamorgana/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.11
'''

View file

@ -16,13 +16,26 @@
Dependencies: Dependencies:
- Python 3.5 or later - Python 3.5 or later
- numpy (optional, no additional functionality) - numpy (optional, faster but no additional functionality)
To get started, try:
```python3
import fatamorgana
help(fatamorgana.OasisLayout)
```
""" """
import pathlib
from .main import OasisLayout, Cell, XName from .main import OasisLayout, Cell, XName
from .basic import NString, AString, Validation, OffsetTable, OffsetEntry, \ from .basic import (
EOFError, SignedError, InvalidDataError, InvalidRecordError NString, AString, Validation, OffsetTable, OffsetEntry,
EOFError, SignedError, InvalidDataError, InvalidRecordError,
UnfilledModalError,
ReuseRepetition, GridRepetition, ArbitraryRepetition
)
__author__ = 'Jan Petykiewicz' __author__ = 'Jan Petykiewicz'
version = '0.4' from .VERSION import __version__
version = __version__

File diff suppressed because it is too large Load diff

View file

@ -3,18 +3,22 @@ This module contains data structures and functions for reading from and
writing to whole OASIS layout files, and provides a few additional writing to whole OASIS layout files, and provides a few additional
abstractions for the data contained inside them. abstractions for the data contained inside them.
""" """
from typing import List, Dict, Union, Optional, Type
import io import io
import logging import logging
from . import records from . import records
from .records import Modals from .records import Modals, Record
from .basic import OffsetEntry, OffsetTable, NString, AString, real_t, Validation, \ from .basic import (
read_magic_bytes, write_magic_bytes, read_uint, EOFError, \ OffsetEntry, OffsetTable, NString, AString, real_t, Validation,
InvalidDataError, InvalidRecordError read_magic_bytes, write_magic_bytes, read_uint, EOFError,
InvalidRecordError,
)
__author__ = 'Jan Petykiewicz' __author__ = 'Jan Petykiewicz'
#logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -23,17 +27,21 @@ class FileModals:
""" """
File-scoped modal variables File-scoped modal variables
""" """
cellname_implicit = None # type: bool or None cellname_implicit: Optional[bool] = None
propname_implicit = None # type: bool or None propname_implicit: Optional[bool] = None
xname_implicit = None # type: bool or None xname_implicit: Optional[bool] = None
textstring_implicit = None # type: bool or None textstring_implicit: Optional[bool] = None
propstring_implicit = None # type: bool or None propstring_implicit: Optional[bool] = None
cellname_implicit = None # type: bool or None
within_cell = False # type: bool property_target: List[records.Property]
within_cblock = False # type: bool
end_has_offset_table = None # type: bool within_cell: bool = False
started = False # type: bool within_cblock: bool = False
end_has_offset_table: bool = False
started: bool = False
def __init__(self, property_target: List[records.Property]):
self.property_target = property_target
class OasisLayout: class OasisLayout:
@ -43,49 +51,50 @@ class OasisLayout:
Names and strings are stored in dicts, indexed by reference number. Names and strings are stored in dicts, indexed by reference number.
Layer names and properties are stored directly using their associated Layer names and properties are stored directly using their associated
record objects. record objects.
Cells are stored using Cell objects (different from Cell record objects). Cells are stored using `Cell` objects (different from `records.Cell`
record objects).
Properties: Attributes:
File properties: (File properties)
.version AString: Version string ('1.0') version (AString): Version string ('1.0')
.unit real number: grid steps per micron unit (real_t): grid steps per micron
.validation Validation: checksum data validation (Validation): checksum data
Names: (Names)
.cellnames Dict[int, NString] cellnames (Dict[int, CellName]): Cell names
.propnames Dict[int, NString] propnames (Dict[int, NString]): Property names
.xnames Dict[int, XName] xnames (Dict[int, XName]): Custom names
Strings: (Strings)
.textstrings Dict[int, AString] textstrings (Dict[int, AString]): Text strings
.propstrings Dict[int, AString] propstrings (Dict[int, AString]): Property strings
Data: (Data)
.layers List[records.LayerName] layers (List[records.LayerName]): Layer definitions
.properties List[records.Property] properties (List[records.Property]): Property values
.cells List[Cell] cells (List[Cell]): Layout cells
""" """
version = None # type: AString version: AString
unit = None # type: real_t unit: real_t
validation = None # type: Validation validation: Validation
properties = None # type: List[records.Property] properties: List[records.Property]
cells = None # type: List[Cell] cells: List['Cell']
cellnames = None # type: Dict[int, NString] cellnames: Dict[int, 'CellName']
propnames = None # type: Dict[int, NString] propnames: Dict[int, NString]
xnames = None # type: Dict[int, XName] xnames: Dict[int, 'XName']
textstrings = None # type: Dict[int, AString]
propstrings = None # type: Dict[int, AString]
layers = None # type: List[records.LayerName]
textstrings: Dict[int, AString]
propstrings: Dict[int, AString]
layers: List[records.LayerName]
def __init__(self, unit: real_t, validation: Validation = None): def __init__(self, unit: real_t, validation: Validation = None):
""" """
:param unit: Real number (i.e. int, float, or Fraction), grid steps per micron. Args:
:param validation: Validation object containing checksum data. unit: Real number (i.e. int, float, or `Fraction`), grid steps per micron.
Default creates a Validation object of the "no checksum" type. validation: `Validation` object containing checksum data.
Default creates a `Validation` object of the "no checksum" type.
""" """
if validation is None: if validation is None:
validation = Validation(0) validation = Validation(0)
@ -105,14 +114,17 @@ class OasisLayout:
@staticmethod @staticmethod
def read(stream: io.BufferedIOBase) -> 'OasisLayout': def read(stream: io.BufferedIOBase) -> 'OasisLayout':
""" """
Read an entire .oas file into an OasisLayout object. Read an entire .oas file into an `OasisLayout` object.
:param stream: Stream to read from. Args:
:return: New OasisLayout object. stream: Stream to read from.
Returns:
New `OasisLayout` object.
""" """
file_state = FileModals() layout = OasisLayout(unit=-1) # dummy unit
modals = Modals() modals = Modals()
layout = OasisLayout(unit=None) file_state = FileModals(layout.properties)
read_magic_bytes(stream) read_magic_bytes(stream)
@ -127,15 +139,20 @@ class OasisLayout:
) -> bool: ) -> bool:
""" """
Read a single record of unspecified type from a stream, adding its Read a single record of unspecified type from a stream, adding its
contents into this OasisLayout object. contents into this `OasisLayout` object.
:param stream: Stream to read from. Args:
:param modals: Modal variable data, used to fill unfilled record stream: Stream to read from.
fields and updated using filled record fields. modals: Modal variable data, used to fill unfilled record
:param file_state: File status data. fields and updated using filled record fields.
:return: True if EOF was reached without error, False otherwise. file_state: File status data.
:raises: InvalidRecordError from unexpected records;
InvalidDataError from within record parsers. Returns:
`True` if EOF was reached without error, `False` otherwise.
Raises:
InvalidRecordError: from unexpected records
InvalidDataError: from within record parsers
""" """
try: try:
record_id = read_uint(stream) record_id = read_uint(stream)
@ -147,6 +164,8 @@ class OasisLayout:
logger.info('read_record of type {} at position 0x{:x}'.format(record_id, stream.tell())) logger.info('read_record of type {} at position 0x{:x}'.format(record_id, stream.tell()))
record: Record
# CBlock # CBlock
if record_id == 34: if record_id == 34:
if file_state.within_cblock: if file_state.within_cblock:
@ -185,16 +204,19 @@ class OasisLayout:
raise InvalidRecordError('Unknown record id: {}'.format(record_id)) raise InvalidRecordError('Unknown record id: {}'.format(record_id))
if record_id == 0: if record_id == 0:
# Pad ''' Pad '''
pass pass
elif record_id == 1: elif record_id == 1:
''' Start '''
record = records.Start.read(stream, record_id) record = records.Start.read(stream, record_id)
record.merge_with_modals(modals) record.merge_with_modals(modals)
self.unit = record.unit self.unit = record.unit
self.version = record.version self.version = record.version
file_state.end_has_offset_table = record.offset_table is None file_state.end_has_offset_table = record.offset_table is None
file_state.property_target = self.properties
# TODO Offset table strict check # TODO Offset table strict check
elif record_id == 2: elif record_id == 2:
''' End '''
record = records.End.read(stream, record_id, file_state.end_has_offset_table) record = records.End.read(stream, record_id, file_state.end_has_offset_table)
record.merge_with_modals(modals) record.merge_with_modals(modals)
self.validation = record.validation self.validation = record.validation
@ -202,6 +224,7 @@ class OasisLayout:
raise InvalidRecordError('Stream continues past End record') raise InvalidRecordError('Stream continues past End record')
return True return True
elif record_id in (3, 4): elif record_id in (3, 4):
''' CellName '''
implicit = record_id == 3 implicit = record_id == 3
if file_state.cellname_implicit is None: if file_state.cellname_implicit is None:
file_state.cellname_implicit = implicit file_state.cellname_implicit = implicit
@ -213,8 +236,12 @@ class OasisLayout:
key = record.reference_number key = record.reference_number
if key is None: if key is None:
key = len(self.cellnames) key = len(self.cellnames)
self.cellnames[key] = record.nstring
cellname = CellName.from_record(record)
self.cellnames[key] = cellname
file_state.property_target = cellname.properties
elif record_id in (5, 6): elif record_id in (5, 6):
''' TextString '''
implicit = record_id == 5 implicit = record_id == 5
if file_state.textstring_implicit is None: if file_state.textstring_implicit is None:
file_state.textstring_implicit = implicit file_state.textstring_implicit = implicit
@ -228,6 +255,7 @@ class OasisLayout:
key = len(self.textstrings) key = len(self.textstrings)
self.textstrings[key] = record.astring self.textstrings[key] = record.astring
elif record_id in (7, 8): elif record_id in (7, 8):
''' PropName '''
implicit = record_id == 7 implicit = record_id == 7
if file_state.propname_implicit is None: if file_state.propname_implicit is None:
file_state.propname_implicit = implicit file_state.propname_implicit = implicit
@ -241,6 +269,7 @@ class OasisLayout:
key = len(self.propnames) key = len(self.propnames)
self.propnames[key] = record.nstring self.propnames[key] = record.nstring
elif record_id in (9, 10): elif record_id in (9, 10):
''' PropString '''
implicit = record_id == 9 implicit = record_id == 9
if file_state.propstring_implicit is None: if file_state.propstring_implicit is None:
file_state.propstring_implicit = implicit file_state.propstring_implicit = implicit
@ -254,17 +283,17 @@ class OasisLayout:
key = len(self.propstrings) key = len(self.propstrings)
self.propstrings[key] = record.astring self.propstrings[key] = record.astring
elif record_id in (11, 12): elif record_id in (11, 12):
''' LayerName '''
record = records.LayerName.read(stream, record_id) record = records.LayerName.read(stream, record_id)
record.merge_with_modals(modals) record.merge_with_modals(modals)
self.layers.append(record) self.layers.append(record)
elif record_id in (28, 29): elif record_id in (28, 29):
''' Property '''
record = records.Property.read(stream, record_id) record = records.Property.read(stream, record_id)
record.merge_with_modals(modals) record.merge_with_modals(modals)
if not file_state.within_cell: file_state.property_target.append(record)
self.properties.append(record)
else:
self.cells[-1].properties.append(record)
elif record_id in (30, 31): elif record_id in (30, 31):
''' XName '''
implicit = record_id == 30 implicit = record_id == 30
if file_state.xname_implicit is None: if file_state.xname_implicit is None:
file_state.xname_implicit = implicit file_state.xname_implicit = implicit
@ -277,25 +306,34 @@ class OasisLayout:
if key is None: if key is None:
key = len(self.xnames) key = len(self.xnames)
self.xnames[key] = XName.from_record(record) self.xnames[key] = XName.from_record(record)
# TODO: do anything with property target?
# #
# Cell and elements # Cell and elements
# #
elif record_id in (13, 14): elif record_id in (13, 14):
''' Cell '''
record = records.Cell.read(stream, record_id) record = records.Cell.read(stream, record_id)
record.merge_with_modals(modals) record.merge_with_modals(modals)
self.cells.append(Cell(record.name)) cell = Cell(record.name)
self.cells.append(cell)
file_state.property_target = cell.properties
elif record_id in (15, 16): elif record_id in (15, 16):
''' XYMode '''
record = records.XYMode.read(stream, record_id) record = records.XYMode.read(stream, record_id)
record.merge_with_modals(modals) record.merge_with_modals(modals)
elif record_id in (17, 18): elif record_id in (17, 18):
''' Placement '''
record = records.Placement.read(stream, record_id) record = records.Placement.read(stream, record_id)
record.merge_with_modals(modals) record.merge_with_modals(modals)
self.cells[-1].placements.append(record) self.cells[-1].placements.append(record)
file_state.property_target = record.properties
elif record_id in _GEOMETRY: elif record_id in _GEOMETRY:
''' Geometry '''
record = _GEOMETRY[record_id].read(stream, record_id) record = _GEOMETRY[record_id].read(stream, record_id)
record.merge_with_modals(modals) record.merge_with_modals(modals)
self.cells[-1].geometry.append(record) self.cells[-1].geometry.append(record)
file_state.property_target = record.properties
else: else:
raise InvalidRecordError('Unknown record id: {}'.format(record_id)) raise InvalidRecordError('Unknown record id: {}'.format(record_id))
return False return False
@ -304,26 +342,33 @@ class OasisLayout:
""" """
Write this object in OASIS fromat to a stream. Write this object in OASIS fromat to a stream.
:param stream: Stream to write to. Args:
:return: Number of bytes written. stream: Stream to write to.
:raises: InvalidDataError if contained records are invalid.
Returns:
Number of bytes written.
Raises:
InvalidDataError: if contained records are invalid.
""" """
modals = Modals() modals = Modals()
size = 0 size = 0
size += write_magic_bytes(stream) size += write_magic_bytes(stream)
size += records.Start(self.unit, self.version).dedup_write(stream, modals) size += records.Start(self.unit, self.version).dedup_write(stream, modals)
size += sum(p.dedup_write(stream, modals) for p in self.properties)
cellnames_offset = OffsetEntry(False, size) cellnames_offset = OffsetEntry(False, size)
size += sum(records.CellName(name, refnum).dedup_write(stream, modals) for refnum, cn in self.cellnames.items():
for refnum, name in self.cellnames.items()) size += records.CellName(cn.nstring, refnum).dedup_write(stream, modals)
size += sum(p.dedup_write(stream, modals) for p in cn.properties)
propnames_offset = OffsetEntry(False, size) propnames_offset = OffsetEntry(False, size)
size += sum(records.PropName(name, refnum).dedup_write(stream, modals) size += sum(records.PropName(name, refnum).dedup_write(stream, modals)
for refnum, name in self.propnames.items()) for refnum, name in self.propnames.items())
xnames_offset = OffsetEntry(False, size) xnames_offset = OffsetEntry(False, size)
size += sum(records.XName(x.attribute, x.string, refnum).dedup_write(stream, modals) size += sum(records.XName(x.attribute, x.bstring, refnum).dedup_write(stream, modals)
for refnum, x in self.xnames.items()) for refnum, x in self.xnames.items())
textstrings_offset = OffsetEntry(False, size) textstrings_offset = OffsetEntry(False, size)
@ -337,8 +382,6 @@ class OasisLayout:
layernames_offset = OffsetEntry(False, size) layernames_offset = OffsetEntry(False, size)
size += sum(r.dedup_write(stream, modals) for r in self.layers) size += sum(r.dedup_write(stream, modals) for r in self.layers)
size += sum(p.dedup_write(stream, modals) for p in self.properties)
size += sum(c.dedup_write(stream, modals) for c in self.cells) size += sum(c.dedup_write(stream, modals) for c in self.cells)
offset_table = OffsetTable( offset_table = OffsetTable(
@ -357,58 +400,110 @@ class Cell:
""" """
Representation of an OASIS cell. Representation of an OASIS cell.
Properties: Attributes:
.name NString or int (CellName reference number) name (Union[NString, int]): name or "CellName reference" number
.properties List of records.Property properties (List[records.Property]): Properties of this cell
.placements List of records.Placement placements (List[records.Placement]): Placement record objects
.geometry List of geometry record objectes geometry: (List[records.geometry_t]): Geometry record objectes
""" """
name = None # type: NString or int name: Union[NString, int]
properties = None # type: List[records.Property] properties: List[records.Property]
placements = None # type: List[records.Placement] placements: List[records.Placement]
geometry = None # type: List[records.geometry_t] geometry: List[records.geometry_t]
def __init__(self, name: NString or int): def __init__(self,
""" name: Union[NString, str, int],
:param name: NString or int (CellName reference number) *,
""" properties: Optional[List[records.Property]] = None,
self.name = name placements: Optional[List[records.Placement]] = None,
self.properties = [] geometry: Optional[List[records.geometry_t]] = None,
self.placements = [] ):
self.geometry = [] self.name = name if isinstance(name, (NString, int)) else NString(name)
self.properties = [] if properties is None else properties
self.placements = [] if placements is None else placements
self.geometry = [] if geometry is None else geometry
def dedup_write(self, stream: io.BufferedIOBase, modals: Modals) -> int: def dedup_write(self, stream: io.BufferedIOBase, modals: Modals) -> int:
""" """
Write this cell to a stream, using the provided modal variables to Write this cell to a stream, using the provided modal variables to
deduplicate any repeated data. deduplicate any repeated data.
:param stream: Stream to write to. Args:
:param modals: Modal variables to use for deduplication. stream: Stream to write to.
:return: Number of bytes written. modals: Modal variables to use for deduplication.
:raises: InvalidDataError if contained records are invalid.
Returns:
Number of bytes written.
Raises:
InvalidDataError: if contained records are invalid.
""" """
size = records.Cell(self.name).dedup_write(stream, modals) size = records.Cell(self.name).dedup_write(stream, modals)
size += sum(p.dedup_write(stream, modals) for p in self.properties) size += sum(p.dedup_write(stream, modals) for p in self.properties)
size += sum(p.dedup_write(stream, modals) for p in self.placements) for placement in self.placements:
size += sum(g.dedup_write(stream, modals) for g in self.geometry) size += placement.dedup_write(stream, modals)
size += sum(p.dedup_write(stream, modals) for p in placement.properties)
for shape in self.geometry:
size += shape.dedup_write(stream, modals)
size += sum(p.dedup_write(stream, modals) for p in shape.properties)
return size return size
class CellName:
"""
Representation of a CellName.
This class is effectively a simplified form of a `records.CellName`,
with the reference data stripped out.
"""
nstring: NString
properties: List[records.Property]
def __init__(self,
nstring: Union[NString, str],
properties: Optional[List[records.Property]] = None):
"""
Args:
nstring: The contained string.
properties: Properties which apply to this CellName's cell, but
are placed following the CellName record.
"""
if isinstance(nstring, NString):
self.nstring = nstring
else:
self.nstring = NString(nstring)
self.properties = [] if properties is None else properties
@staticmethod
def from_record(record: records.CellName) -> 'CellName':
"""
Create an `CellName` object from a `records.CellName` record.
Args:
record: CellName record to use.
Returns:
A new `CellName` object.
"""
return CellName(record.nstring)
class XName: class XName:
""" """
Representation of an XName. Representation of an XName.
This class is effectively a simplified form of a records.XName, This class is effectively a simplified form of a `records.XName`,
with the reference data stripped out. with the reference data stripped out.
""" """
attribute = None # type: int attribute: int
bstring = None # type: bytes bstring: bytes
def __init__(self, attribute: int, bstring: bytes): def __init__(self, attribute: int, bstring: bytes):
""" """
:param attribute: Attribute number. Args:
:param bstring: Binary data. attribute: Attribute number.
bstring: Binary data.
""" """
self.attribute = attribute self.attribute = attribute
self.bstring = bstring self.bstring = bstring
@ -416,16 +511,19 @@ class XName:
@staticmethod @staticmethod
def from_record(record: records.XName) -> 'XName': def from_record(record: records.XName) -> 'XName':
""" """
Create an XName object from a records.XName record. Create an `XName` object from a `records.XName` record.
:param record: XName record to use. Args:
:return: XName object. record: XName record to use.
Returns:
a new `XName` object.
""" """
return XName(record.attribute, record.bstring) return XName(record.attribute, record.bstring)
# Mapping from record id to record class. # Mapping from record id to record class.
_GEOMETRY = { _GEOMETRY: Dict[int, Type[records.geometry_t]] = {
19: records.Text, 19: records.Text,
20: records.Rectangle, 20: records.Rectangle,
21: records.Polygon, 21: records.Polygon,

0
fatamorgana/py.typed Normal file
View file

File diff suppressed because it is too large Load diff

View file

@ -1,19 +1,44 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from setuptools import setup, find_packages from setuptools import setup, find_packages
import fatamorgana
with open('README.md', 'r') as f: with open('README.md', 'r') as f:
long_description = f.read() long_description = f.read()
with open('fatamorgana/VERSION.py', 'rt') as f:
version = f.readlines()[2].strip()
setup(name='fatamorgana', setup(name='fatamorgana',
version=fatamorgana.version, version=version,
description='OASIS layout format parser and writer', description='OASIS layout format parser and writer',
long_description=long_description, long_description=long_description,
long_description_content_type='text/markdown', long_description_content_type='text/markdown',
author='Jan Petykiewicz', author='Jan Petykiewicz',
author_email='anewusername@gmail.com', author_email='anewusername@gmail.com',
url='https://mpxd.net/code/jan/fatamorgana', url='https://mpxd.net/code/jan/fatamorgana',
packages=find_packages(),
package_data={
'fatamorgana': ['py.typed'],
},
install_requires=[
'typing',
],
extras_require={
'numpy': ['numpy'],
},
classifiers=[
'Programming Language :: Python :: 3',
'Development Status :: 3 - Alpha',
'Environment :: Other Environment',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'Intended Audience :: Manufacturing',
'Intended Audience :: Science/Research',
'License :: OSI Approved :: GNU Affero General Public License v3',
'Topic :: Scientific/Engineering',
'Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)',
],
keywords=[ keywords=[
'OASIS', 'OASIS',
'layout', 'layout',
@ -34,28 +59,5 @@ setup(name='fatamorgana',
'polygon', 'polygon',
'gds', 'gds',
], ],
classifiers=[
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Development Status :: 3 - Alpha',
'Environment :: Other Environment',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'Intended Audience :: Manufacturing',
'Intended Audience :: Science/Research',
'License :: OSI Approved :: GNU Affero General Public License v3',
'Operating System :: POSIX :: Linux',
'Operating System :: Microsoft :: Windows',
'Topic :: Scientific/Engineering',
'Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)',
'Topic :: Software Development :: Libraries :: Python Modules',
],
packages=find_packages(),
install_requires=[
'typing',
],
extras_require={
'numpy': ['numpy'],
},
) )